Skip to content

Commit 55fb491

Browse files
committed
fixes #34
1 parent 049658e commit 55fb491

File tree

4 files changed

+103
-26
lines changed

4 files changed

+103
-26
lines changed

readme.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Find more advanced examples in [Show & Tell](https://github.com/dbuezas/lovelace
2222

2323
## Installation via [HACS](https://hacs.xyz/)
2424

25-
* Search for `Plotly Graph Card`.
25+
- Search for `Plotly Graph Card`.
2626

2727
## Card Config
2828

@@ -139,7 +139,7 @@ entities:
139139
- sensor.temperature2
140140
color_scheme: dutch_field
141141
# or use numbers instead 0 to 24 available:
142-
# color_scheme: 1
142+
# color_scheme: 1
143143
# or pass your color scheme
144144
# color_scheme: ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","red"]
145145
```
@@ -322,6 +322,33 @@ no_theme: true
322322
To define general configurations like enabling scroll to zoom, disabling the modebar, etc.
323323
Anything from https://plotly.com/javascript/configuration-options/.
324324

325+
## significant_changes_only
326+
327+
When true, will tell HA to only fetch datapoints with a different state as the one before.
328+
More here: https://developers.home-assistant.io/docs/api/rest/ under `/api/history/period/<timestamp>`
329+
330+
Caveats:
331+
1. zana-37 repoorts that `minimal_response: false` needs to be set to get all non-significant datapoints [here](https://github.com/dbuezas/lovelace-plotly-graph-card/issues/34#issuecomment-1085083597).
332+
2. This configuration will be ignored (will be true) while fetching [Attribute Values](#Attribute-Values).
333+
334+
```yaml
335+
significant_changes_only: true # defaults to false
336+
```
337+
338+
## minimal_response
339+
340+
When true, tell HA to only return last_changed and state for states other than the first and last state (much faster).
341+
More here: https://developers.home-assistant.io/docs/api/rest/ under `/api/history/period/<timestamp>`
342+
343+
Caveats:
344+
1. This configuration will be ignored (will be true) while fetching [Attribute Values](#Attribute-Values).
345+
346+
```yaml
347+
minimal_response: false # defaults to true
348+
```
349+
350+
Update data every `refresh_interval` seconds. Use `0` or delete the line to disable updates
351+
325352
## hours_to_show:
326353

327354
How many hours are shown.
@@ -340,7 +367,6 @@ Update data every `refresh_interval` seconds. Use `0` or delete the line to disa
340367
- ATTENTION: The development card is `type: custom:plotly-graph-dev`
341368
- Either use Safari or Disbale [chrome://flags/#block-insecure-private-network-requests](chrome://flags/#block-insecure-private-network-requests): Chrome doesn't allow public network resources from requesting private-network resources - unless the public-network resource is secure (HTTPS) and the private-network resource provides appropriate (yet-undefined) CORS headers. More [here](https://stackoverflow.com/questions/66534759/chrome-cors-error-on-request-to-localhost-dev-server-from-remote-site)
342369

343-
344370
# Build
345371

346372
`npm run build`

src/Cache.ts

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,23 @@ export function mapValues<T, S>(
1313
async function fetchSingleRange(
1414
hass: HomeAssistant,
1515
entityIdWithAttribute: string,
16-
[startT, endT]: number[]
16+
[startT, endT]: number[],
17+
significant_changes_only: boolean,
18+
minimal_response: boolean
1719
) {
1820
const start = new Date(startT);
1921
const end = new Date(endT);
2022
const [entityId2, attribute] = entityIdWithAttribute.split("::");
21-
const minimal_response = !attribute ? "&minimal_response" : "";
23+
const minimal_response_query =
24+
minimal_response && !attribute ? "minimal_response&" : "";
25+
const significant_changes_only_query =
26+
significant_changes_only && !attribute ? "1" : "0";
2227
const uri =
2328
`history/period/${start.toISOString()}?` +
2429
`filter_entity_id=${entityId2}&` +
25-
`significant_changes_only=1` +
26-
minimal_response +
27-
`&end_time=${end.toISOString()}`;
30+
`significant_changes_only=${significant_changes_only_query}&` +
31+
minimal_response_query +
32+
`end_time=${end.toISOString()}`;
2833
let list: History | undefined;
2934
let succeeded = false;
3035
let retries = 0;
@@ -37,13 +42,30 @@ async function fetchSingleRange(
3742
console.error(e);
3843
retries++;
3944
if (retries > 50) return null;
40-
await sleep(100)
45+
await sleep(100);
4146
}
4247
}
43-
if (!list) return null;
48+
if (!list || list.length == 0) return null;
49+
50+
/*
51+
home assistant will "invent" a datapoiont at startT with the previous known value, except if there is actually one at startT.
52+
To avoid these duplicates, the "fetched range" is capped to end at the last known point instead of endT.
53+
This ensures that the next fetch will start with a duplicate of the last known datapoint, which can then be removed.
54+
On top of that, in order to ensure that the last known point is extended to endT, I duplicate the last datapoint
55+
and set its date to endT.
56+
*/
57+
const last = list[list.length - 1];
58+
const dup = JSON.parse(JSON.stringify(last));
59+
list[0].duplicate_datapoint = true;
60+
dup.duplicate_datapoint = true;
61+
dup.last_changed = Math.min(endT, Date.now());
62+
list.push(dup);
4463
return {
4564
entityId: entityIdWithAttribute,
46-
range: [startT, Math.min(+new Date(), endT)], // cap range to now
65+
range: [
66+
startT,
67+
+new Date(attribute ? last.last_updated : last.last_changed),
68+
], // cap range to now
4769
attributes: {
4870
unit_of_measurement: "",
4971
...list[0].attributes,
@@ -70,11 +92,23 @@ export default class Cache {
7092
range: TimestampRange,
7193
removeOutsideRange: boolean,
7294
entityNames: string[],
73-
hass: HomeAssistant
95+
hass: HomeAssistant,
96+
significant_changes_only: boolean,
97+
minimal_response: boolean
7498
) {
99+
entityNames = Array.from(new Set(entityNames)); // remove duplicates
75100
return (this.busy = this.busy
76101
.catch(() => {})
77-
.then(() => this._update(range, removeOutsideRange, entityNames, hass)));
102+
.then(() =>
103+
this._update(
104+
range,
105+
removeOutsideRange,
106+
entityNames,
107+
hass,
108+
significant_changes_only,
109+
minimal_response
110+
)
111+
));
78112
}
79113

80114
private removeOutsideRange(range: TimestampRange) {
@@ -108,7 +142,9 @@ export default class Cache {
108142
range: TimestampRange,
109143
removeOutsideRange: boolean,
110144
entityNames: string[],
111-
hass: HomeAssistant
145+
hass: HomeAssistant,
146+
significant_changes_only: boolean,
147+
minimal_response: boolean
112148
) {
113149
if (removeOutsideRange) {
114150
this.removeOutsideRange(range);
@@ -120,26 +156,27 @@ export default class Cache {
120156
entityNames.flatMap((entityId) => {
121157
const rangesToFetch = subtractRanges([range], this.ranges[entityId]);
122158
return rangesToFetch.map((aRange) =>
123-
fetchSingleRange(hass, entityId, aRange)
159+
fetchSingleRange(
160+
hass,
161+
entityId,
162+
aRange,
163+
significant_changes_only,
164+
minimal_response
165+
)
124166
);
125167
})
126168
);
127169
const fetchedHistories = (await fetchedHistoriesP).filter(isTruthy);
128170
// add to existing histories
129171
for (const fetchedHistory of fetchedHistories) {
130172
const { entityId } = fetchedHistory;
131-
const h = (this.histories[entityId] ??= []);
173+
let h = (this.histories[entityId] ??= []);
132174
h.push(...fetchedHistory.history);
133175
h.sort((a, b) => a.last_changed - b.last_changed);
134-
// remove the rogue datapoint home assistant creates when there is no new data
135-
const [prev, last] = h.slice(-2);
136-
const isRepeated =
137-
prev?.last_changed === last?.last_changed - 1 &&
138-
prev?.state === last?.state;
139-
if (isRepeated) {
140-
// remove the old one
141-
h.splice(h.length - 2, 1);
142-
}
176+
h = h.filter(
177+
(x, i) => i == 0 || i == h.length - 1 || !x.duplicate_datapoint
178+
);
179+
this.histories[entityId] = h;
143180
this.attributes[entityId] = fetchedHistory.attributes;
144181
this.ranges[entityId].push(fetchedHistory.range);
145182
this.ranges[entityId] = compactRanges(this.ranges[entityId]);

src/plotly-graph-card.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ export class PlotlyGraph extends HTMLElement {
248248
},
249249
no_theme: config.no_theme ?? false,
250250
no_default_layout: config.no_default_layout ?? false,
251+
significant_changes_only: config.significant_changes_only ?? false,
252+
minimal_response: config.significant_changes_only ?? true,
251253
};
252254

253255
const was = this.config;
@@ -267,7 +269,14 @@ export class PlotlyGraph extends HTMLElement {
267269
)
268270
);
269271
while (!this.hass) await sleep(100);
270-
await this.cache.update(range, !this.isBrowsing, entityNames, this.hass);
272+
await this.cache.update(
273+
range,
274+
!this.isBrowsing,
275+
entityNames,
276+
this.hass,
277+
this.config.minimal_response,
278+
this.config.significant_changes_only
279+
);
271280
await this.plot();
272281
};
273282
getAllUnitsOfMeasurement() {

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export type InputConfig = {
2323
config?: Partial<Plotly.Config>;
2424
no_theme?: boolean;
2525
no_default_layout?: boolean;
26+
significant_changes_only?: boolean; // defaults to false
27+
minimal_response?: boolean; // defaults to true
2628
};
2729
export type Config = {
2830
hours_to_show: number;
@@ -39,9 +41,12 @@ export type Config = {
3941
config: Partial<Plotly.Config>;
4042
no_theme: boolean;
4143
no_default_layout: boolean;
44+
significant_changes_only: boolean,
45+
minimal_response: boolean
4246
};
4347
export type Timestamp = number;
4448
export type History = {
49+
duplicate_datapoint?: true
4550
entity_id: string;
4651
last_changed: Timestamp;
4752
last_updated: Timestamp;

0 commit comments

Comments
 (0)