Skip to content

Commit 5933916

Browse files
authored
Add refresh_interval: auto (#114)
* WIP Add refresh_interval: auto * Add attribute support to automatic refresh * Disable extending datapoints to Date.now() * Re-add extending datapoints up to present, but now only during plotting * Extend last datapoint into the present while plotting (not inside the cache) * Impl. `refresh_interval: auto` for statistics * Tweak ranges so no APIs are touched when refreshing if the visible range is already cached
1 parent 9e2e770 commit 5933916

File tree

6 files changed

+170
-105
lines changed

6 files changed

+170
-105
lines changed

readme.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,16 +426,22 @@ Caveats:
426426
minimal_response: false # defaults to true
427427
```
428428

429-
Update data every `refresh_interval` seconds. Use `0` or delete the line to disable updates
430-
431429
## hours_to_show:
432430

433431
How many hours are shown.
434432
Exactly the same as the history card, except decimal values (e.g `0.1`) do actually work
435433

436434
## refresh_interval:
437435

438-
Update data every `refresh_interval` seconds. Use `0` or delete the line to disable updates
436+
Update data every `refresh_interval` seconds.
437+
438+
Examples:
439+
440+
```yaml
441+
refresh_interval: auto # (default) update automatically when an entity changes its state.
442+
refresh_interval: 0 # never update.
443+
refresh_interval: 5 # update every 5 seconds
444+
```
439445

440446
# Development
441447

src/cache/Cache.ts

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import fetchStatistics from "./fetch-statistics";
44
import fetchStates from "./fetch-states";
55
import {
66
TimestampRange,
7-
History,
87
isEntityIdAttrConfig,
98
EntityConfig,
109
isEntityIdStateConfig,
1110
isEntityIdStatisticsConfig,
1211
HistoryInRange,
12+
EntityState,
1313
} from "../types";
1414

1515
export function mapValues<T, S>(
@@ -25,13 +25,14 @@ async function fetchSingleRange(
2525
significant_changes_only: boolean,
2626
minimal_response: boolean
2727
): Promise<HistoryInRange> {
28-
const start = new Date(startT);
28+
const start = new Date(startT - 1);
29+
endT = Math.min(endT, Date.now());
2930
const end = new Date(endT);
30-
let historyInRange: HistoryInRange;
31+
let history: EntityState[];
3132
if (isEntityIdStatisticsConfig(entity)) {
32-
historyInRange = await fetchStatistics(hass, entity, [start, end]);
33+
history = await fetchStatistics(hass, entity, [start, end]);
3334
} else {
34-
historyInRange = await fetchStates(
35+
history = await fetchStates(
3536
hass,
3637
entity,
3738
[start, end],
@@ -40,25 +41,19 @@ async function fetchSingleRange(
4041
);
4142
}
4243

43-
const { history, range } = historyInRange;
4444
/*
45-
home assistant will "invent" a datapoiont at startT with the previous known value, except if there is actually one at startT.
46-
To avoid these duplicates, the "fetched range" is capped to end at the last known point instead of endT.
47-
This ensures that the next fetch will start with a duplicate of the last known datapoint, which can then be removed.
48-
On top of that, in order to ensure that the last known point is extended to endT, I duplicate the last datapoint
49-
and set its date to endT.
45+
home assistant will "invent" a datapoiont at startT with the previous
46+
known value, except if there is actually one at startT.
47+
To avoid these duplicates, the "fetched range" starts at startT-1,
48+
but the first point is marked to be deleted (fake_boundary_datapoint).
49+
Delettion occurs when merging the fetched range inside the cached history.
5050
*/
51+
let range: [number, number] = [startT, endT];
5152
if (history.length) {
52-
const last = history[history.length - 1];
53-
const dup = JSON.parse(JSON.stringify(last));
54-
history[0].duplicate_datapoint = true;
55-
dup.duplicate_datapoint = true;
56-
dup.last_updated = Math.min(+end, Date.now());
57-
history.push(dup);
53+
history[0].fake_boundary_datapoint = true;
5854
}
59-
Math.min(+end, Date.now());
6055
return {
61-
range: [range[0], Math.min(range[1], Date.now())],
56+
range,
6257
history,
6358
};
6459
}
@@ -75,8 +70,22 @@ export function getEntityKey(entity: EntityConfig) {
7570
}
7671
export default class Cache {
7772
ranges: Record<string, TimestampRange[]> = {};
78-
histories: Record<string, History> = {};
73+
histories: Record<string, EntityState[]> = {};
7974
busy = Promise.resolve(); // mutex
75+
76+
add(entity: EntityConfig, states: EntityState[], range: [number, number]) {
77+
const entityKey = getEntityKey(entity);
78+
let h = (this.histories[entityKey] ??= []);
79+
h.push(...states);
80+
h.sort((a, b) => a.timestamp - b.timestamp);
81+
h = h.filter((x, i) => i == 0 || !x.fake_boundary_datapoint);
82+
h = h.filter((_, i) => h[i - 1]?.timestamp !== h[i].timestamp);
83+
this.histories[entityKey] = h;
84+
this.ranges[entityKey] ??= [];
85+
this.ranges[entityKey].push(range);
86+
this.ranges[entityKey] = compactRanges(this.ranges[entityKey]);
87+
}
88+
8089
clearCache() {
8190
this.ranges = {};
8291
this.histories = {};
@@ -112,47 +121,34 @@ export default class Cache {
112121
minimal_response
113122
);
114123
if (fetchedHistory === null) continue;
115-
let h = (this.histories[entityKey] ??= []);
116-
h.push(...fetchedHistory.history);
117-
h.sort((a, b) => a.last_updated - b.last_updated);
118-
h = h.filter(
119-
(x, i) => i == 0 || i == h.length - 1 || !x.duplicate_datapoint
120-
);
121-
h = h.filter(
122-
(_, i) => h[i].last_updated !== h[i + 1]?.last_updated
123-
);
124-
this.histories[entityKey] = h;
125-
this.ranges[entityKey].push(fetchedHistory.range);
126-
this.ranges[entityKey] = compactRanges(this.ranges[entityKey]);
124+
this.add(entity, fetchedHistory.history, fetchedHistory.range);
127125
}
128126
});
129127

130128
await Promise.all(promises);
131129
}));
132130
}
133131

134-
private removeOutsideRange(range: TimestampRange) {
132+
removeOutsideRange(range: TimestampRange) {
135133
this.ranges = mapValues(this.ranges, (ranges) =>
136134
subtractRanges(ranges, [
137135
[Number.NEGATIVE_INFINITY, range[0] - 1],
138136
[range[1] + 1, Number.POSITIVE_INFINITY],
139137
])
140138
);
141139
this.histories = mapValues(this.histories, (history) => {
142-
let first: History[0] | undefined;
143-
let last: History[0] | undefined;
140+
let first: EntityState | undefined;
141+
let last: EntityState | undefined;
144142
const newHistory = history.filter((datum) => {
145-
if (datum.last_updated <= range[0]) first = datum;
146-
else if (!last && datum.last_updated >= range[1]) last = datum;
143+
if (datum.timestamp <= range[0]) first = datum;
144+
else if (!last && datum.timestamp >= range[1]) last = datum;
147145
else return true;
148146
return false;
149147
});
150148
if (first) {
151-
first.last_updated = range[0];
152149
newHistory.unshift(first);
153150
}
154151
if (last) {
155-
last.last_updated = range[1];
156152
newHistory.push(last);
157153
}
158154
return newHistory;

src/cache/fetch-states.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import { HomeAssistant } from "custom-card-helpers";
22
import {
33
EntityIdAttrConfig,
44
EntityIdStateConfig,
5-
History,
6-
HistoryInRange,
5+
EntityState,
6+
HassEntity,
77
isEntityIdAttrConfig,
88
isEntityIdStateConfig,
99
} from "../types";
10-
import { sleep } from "../utils";
1110

1211
async function fetchStates(
1312
hass: HomeAssistant,
1413
entity: EntityIdStateConfig | EntityIdAttrConfig,
1514
[start, end]: [Date, Date],
1615
significant_changes_only?: boolean,
1716
minimal_response?: boolean
18-
): Promise<HistoryInRange> {
17+
): Promise<EntityState[]> {
18+
const no_attributes_query = isEntityIdStateConfig(entity)
19+
? "no_attributes&"
20+
: "";
1921
const minimal_response_query =
2022
minimal_response && isEntityIdStateConfig(entity)
2123
? "minimal_response&"
@@ -26,11 +28,12 @@ async function fetchStates(
2628
`history/period/${start.toISOString()}?` +
2729
`filter_entity_id=${entity.entity}&` +
2830
`significant_changes_only=${significant_changes_only_query}&` +
31+
`${no_attributes_query}&` +
2932
minimal_response_query +
3033
`end_time=${end.toISOString()}`;
31-
let list: History | undefined;
34+
let list: HassEntity[] | undefined;
3235
try {
33-
const lists: History[] = (await hass.callApi("GET", uri)) || [];
36+
const lists: HassEntity[][] = (await hass.callApi("GET", uri)) || [];
3437
list = lists[0];
3538
} catch (e: any) {
3639
console.error(e);
@@ -41,17 +44,14 @@ async function fetchStates(
4144
);
4245
}
4346
if (!list) list = [];
44-
return {
45-
range: [+start, +end],
46-
history: list
47-
.map((entry) => ({
48-
...entry,
49-
state: isEntityIdAttrConfig(entity)
50-
? entry.attributes[entity.attribute] || null
51-
: entry.state,
52-
last_updated: +new Date(entry.last_updated || entry.last_changed),
53-
}))
54-
.filter(({ last_updated }) => last_updated),
55-
};
47+
return list
48+
.map((entry) => ({
49+
...entry,
50+
value: isEntityIdAttrConfig(entity)
51+
? entry.attributes[entity.attribute] || null
52+
: entry.state,
53+
timestamp: +new Date(entry.last_updated || entry.last_changed),
54+
}))
55+
.filter(({ timestamp }) => timestamp);
5656
}
5757
export default fetchStates;

src/cache/fetch-statistics.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { HomeAssistant } from "custom-card-helpers";
22
import { Statistics, StatisticValue } from "../recorder-types";
3-
import { EntityIdStatisticsConfig, HistoryInRange } from "../types";
4-
import { sleep } from "../utils";
3+
import { EntityIdStatisticsConfig, EntityState } from "../types";
54

65
async function fetchStatistics(
76
hass: HomeAssistant,
87
entity: EntityIdStatisticsConfig,
98
[start, end]: [Date, Date]
10-
): Promise<HistoryInRange> {
9+
): Promise<EntityState[]> {
1110
let statistics: StatisticValue[] | null = null;
1211
try {
1312
const statsP = hass.callWS<Statistics>({
@@ -27,18 +26,10 @@ async function fetchStatistics(
2726
);
2827
}
2928
if (!statistics) statistics = []; //throw new Error(`Error fetching ${entity.entity}`);
30-
return {
31-
range: [+start, +end],
32-
history: statistics.map((entry) => {
33-
return {
34-
entity_id: entry.statistic_id,
35-
last_updated: +new Date(entry.start),
36-
last_changed: +new Date(entry.start),
37-
state: entry[entity.statistic] ?? "",
38-
statistics: entry,
39-
attributes: {},
40-
};
41-
}),
42-
};
29+
return statistics.map((entry) => ({
30+
...entry,
31+
timestamp: +new Date(entry.start),
32+
value: entry[entity.statistic] ?? "",
33+
}));
4334
}
4435
export default fetchStatistics;

0 commit comments

Comments
 (0)