Skip to content

Commit cf8f7cd

Browse files
author
David Buezas
committed
with recoil. Feels wrong
1 parent 8bec604 commit cf8f7cd

File tree

5 files changed

+175
-125
lines changed

5 files changed

+175
-125
lines changed

src/Card.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import { HomeAssistant } from "custom-card-helpers"; // This is a community main
44
import * as themes from "./themes";
55
import StyleHack from "./StyleHack";
66
import merge from "lodash-es/merge";
7-
import { useData, useWidth } from "./hooks";
7+
import { dataAtom, useWidth, isLoadingAtom, rangeAtom } from "./hooks";
88
import { Config, DateRange } from "./types";
99
import sub from "date-fns/sub";
10+
import {
11+
atom,
12+
useRecoilState,
13+
useRecoilValue,
14+
useSetRecoilState,
15+
} from "recoil";
16+
import { WithCache } from "./cache";
1017

1118
declare module "preact/src/jsx" {
1219
namespace JSXInternal {
@@ -34,15 +41,35 @@ type Props = {
3441
hass?: HomeAssistant;
3542
config: Config;
3643
};
44+
45+
export const EntitiesAtom = atom<Config["entities"]>({
46+
key: "ConfigEntitiesAtom",
47+
default: [],
48+
});
49+
export const HassAtom = atom<HomeAssistant | undefined>({
50+
key: "HassAtom",
51+
default: undefined,
52+
dangerouslyAllowMutability: true,
53+
});
54+
3755
const Plotter = ({ config, hass }: Props) => {
56+
config = JSON.parse(JSON.stringify(config));
57+
const [entities, setEntities] = useRecoilState(EntitiesAtom);
58+
if (JSON.stringify(config.entities) !== JSON.stringify(entities))
59+
setEntities(config.entities);
60+
const [storedHass, setStoredHass] = useRecoilState(HassAtom);
61+
if (!storedHass && hass) setStoredHass(hass);
62+
3863
const layoutRef = useRef<Partial<Plotly.Layout>>({});
3964
const container = useRef<HTMLDivElement>(null);
4065
const width = useWidth(container.current);
41-
const [range, setRange] = useState<DateRange>(() => {
66+
const [range, setRange] = useRecoilState(rangeAtom);
67+
useEffect(() => {
4268
const minutes = Number(config.hours_to_show) * 60; // if add hours is used, decimals are ignored
43-
return [sub(new Date(), { minutes }), new Date()];
44-
});
45-
const { data, isLoading } = useData(hass, config, range);
69+
setRange([sub(new Date(), { minutes }), new Date()]);
70+
}, []);
71+
const isLoading = useRecoilValue(isLoadingAtom);
72+
const data = useRecoilValue(dataAtom);
4673
const resetRange = useCallback(() => {
4774
const minutes = Number(config.hours_to_show) * 60; // if add hours is used, decimals are ignored
4875
setRange([sub(new Date(), { minutes }), new Date()]);
@@ -60,7 +87,6 @@ const Plotter = ({ config, hass }: Props) => {
6087
useEffect(() => {
6188
if (!container.current || width === 0) return;
6289
const element = container.current;
63-
console.log("layoutRef.current", layoutRef.current);
6490
layoutRef.current = merge(
6591
extractRanges(layoutRef.current),
6692
themes[config.theme!] || themes.dark,
@@ -87,7 +113,6 @@ const Plotter = ({ config, hass }: Props) => {
87113
resetRange();
88114
}
89115
if (eventdata["xaxis.range[0]"]) {
90-
console.log("layout event");
91116
setRange([
92117
new Date(eventdata["xaxis.range[0]"]),
93118
new Date(eventdata["xaxis.range[1]"]),
@@ -110,13 +135,14 @@ const Plotter = ({ config, hass }: Props) => {
110135
try {
111136
Plotly.relayout(container.current, {
112137
// 'yaxis.range': TODO:
113-
"xaxis.range": range,
138+
"xaxis.range": range.slice(),
114139
});
115140
} catch (e) {}
116141
}, [range, container.current]);
117142
return (
118143
<ha-card>
119144
<StyleHack />
145+
<WithCache />
120146
<div ref={container}></div>
121147
</ha-card>
122148
);

src/cache.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.

src/cache.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { memo, useEffect, useRef } from "preact/compat";
2+
3+
import { atom, useRecoilValue, useSetRecoilState } from "recoil";
4+
import { EntitiesAtom, HassAtom } from "./Card";
5+
import { compactRanges, subtractRanges } from "./date-ranges";
6+
import { rangeAtom } from "./hooks";
7+
import { DateRange, History } from "./types";
8+
9+
export type Cache = {
10+
ranges: DateRange[];
11+
histories: Record<string, History>;
12+
};
13+
14+
export const cacheAtom = atom<Cache>({
15+
key: "cacheAtom",
16+
default: {
17+
ranges: [],
18+
histories: {},
19+
},
20+
});
21+
22+
// @todo: implement with recoil snapshots or st.
23+
export const WithCache = memo(() => {
24+
const range = useRecoilValue(rangeAtom);
25+
const entities = useRecoilValue(EntitiesAtom);
26+
const hass = useRecoilValue(HassAtom);
27+
let setCache = useSetRecoilState(cacheAtom);
28+
const promise = useRef(Promise.resolve());
29+
const updatedData = useRef({ range, entities });
30+
updatedData.current = { range, entities };
31+
const lastCacheRef = useRef<Cache>({
32+
// @TODO: find way to avoid this hack that solves the cyclic dependency
33+
ranges: [],
34+
histories: {},
35+
});
36+
37+
useEffect(() => {
38+
if (!hass) return;
39+
console.log("fetchiing");
40+
const fetch = async () => {
41+
const { range, entities } = updatedData.current;
42+
const entityNames = entities.map(({ entity }) => entity) || [];
43+
let cache = lastCacheRef.current;
44+
if (
45+
JSON.stringify(Object.keys(cache.histories)) !=
46+
JSON.stringify(entityNames)
47+
) {
48+
// entity names changed, clear cache
49+
cache = {
50+
ranges: [],
51+
histories: {},
52+
};
53+
}
54+
const rangesToFetch = subtractRanges([range], cache.ranges);
55+
const fetchedHistories = await Promise.all(
56+
rangesToFetch.map(async ([start, end]) => {
57+
const uri =
58+
`history/period/${start.toISOString()}?` +
59+
`filter_entity_id=${entityNames}&` +
60+
`significant_changes_only=1&` +
61+
`minimal_response&end_time=${end.toISOString()}`;
62+
const r: History[] = (await hass.callApi("GET", uri)) || [];
63+
// @TODO: hass doesn't return an empty array for empty histories, so data is out of order!!!!
64+
console.log("added:", r[0]?.length);
65+
console.log(
66+
start.toTimeString().split(" ")[0] +
67+
"-" +
68+
end.toTimeString().split(" ")[0]
69+
);
70+
return r.map((history) =>
71+
history?.map((entry) => ({
72+
...entry,
73+
last_changed: new Date(entry.last_changed),
74+
}))
75+
);
76+
})
77+
);
78+
79+
const histories: Cache["histories"] = {};
80+
entityNames.forEach((name, i) => {
81+
histories[name] = cache.histories[name]?.slice() || [];
82+
for (const history of fetchedHistories) {
83+
history[i] = history[i] || [];
84+
histories[name] = [...histories[name], ...history[i]];
85+
}
86+
});
87+
let lastKnwonTimestamp = 0;
88+
entityNames.forEach((name) => {
89+
histories[name].sort(
90+
(a, b) => a.last_changed.getTime() - b.last_changed.getTime()
91+
);
92+
const timestamp = +histories[name].slice(-1)[0].last_changed + 1;
93+
lastKnwonTimestamp = Math.max(lastKnwonTimestamp, timestamp);
94+
});
95+
96+
let ranges = [...cache.ranges, ...rangesToFetch];
97+
var MAX_TIMESTAMP = 8640000000000000;
98+
ranges = subtractRanges(ranges, [
99+
[new Date(lastKnwonTimestamp), new Date(MAX_TIMESTAMP)],
100+
]);
101+
102+
const newCache = {
103+
ranges: compactRanges(ranges),
104+
histories,
105+
entityNames,
106+
};
107+
lastCacheRef.current = newCache;
108+
setCache(newCache);
109+
};
110+
promise.current = promise.current.then(fetch);
111+
}, [{}]);
112+
return <></>;
113+
});

src/hooks.ts

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,38 @@
1-
import { useEffect, useRef, useState } from "preact/hooks";
2-
import { HomeAssistant } from "custom-card-helpers"; // This is a community maintained npm module with common helper functions/types. https://github.com/custom-cards/custom-card-helpers
3-
import { Config, DateRange } from "./types";
4-
import { Cache, addToCache } from "./cache";
1+
import { useEffect, useState } from "preact/hooks";
2+
import { DateRange } from "./types";
3+
import { cacheAtom } from "./cache";
54

6-
const useHistory = (
7-
hass: HomeAssistant | undefined,
8-
config: Config,
9-
range: DateRange
10-
) => {
11-
const [cache, setCache] = useState<Cache>({
12-
ranges: [],
13-
histories: {},
14-
});
15-
const [isLoading, setIsLoading] = useState(false);
16-
const entityNames = config.entities?.map(({ entity }) => entity) || [];
17-
useEffect(() => {
18-
if (!hass) return;
19-
const fetch = async () => {
20-
setIsLoading(true);
21-
setCache(await addToCache(cache, range, hass, entityNames));
22-
setIsLoading(false);
23-
};
24-
fetch();
25-
}, [!!hass, range, entityNames.toString()]);
26-
return { history: cache.histories, isLoading };
27-
};
5+
import { atom, selector } from "recoil";
6+
import { EntitiesAtom } from "./Card";
287

29-
export const useData = (
30-
hass: HomeAssistant | undefined,
31-
config: Config,
32-
range: DateRange
33-
) => {
34-
const { history, isLoading } = useHistory(hass, config, range);
35-
const [data, setData] = useState<Plotly.Data[]>([]);
36-
useEffect(() => {
37-
const data: Plotly.Data[] = config.entities.map((trace) => {
8+
export const isLoadingAtom = atom({
9+
key: "isLoadingAtom",
10+
default: false,
11+
});
12+
13+
export const rangeAtom = atom<DateRange>({
14+
key: "rangeAtom",
15+
default: [new Date(), new Date()],
16+
});
17+
18+
export const dataAtom = selector<Plotly.Data[]>({
19+
key: "dataAtom",
20+
get: ({ get }) => {
21+
const entities = get(EntitiesAtom);
22+
const { histories } = get(cacheAtom);
23+
24+
const data: Plotly.Data[] = entities.map((trace) => {
3825
const name = trace.entity;
3926
return {
4027
name,
4128
...trace,
42-
x: history[name]?.map(({ last_changed }) => last_changed),
43-
y: history[name]?.map(({ state }) => state),
29+
x: histories[name]?.map(({ last_changed }) => last_changed),
30+
y: histories[name]?.map(({ state }) => state),
4431
};
4532
});
46-
setData(data);
47-
}, [history, JSON.stringify(config.entities)]);
48-
return { data, isLoading };
49-
};
33+
return data;
34+
},
35+
});
5036

5137
export const useWidth = (element: HTMLDivElement | null) => {
5238
const [width, setWidth] = useState(0);

src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function App(props) {
1111
);
1212
}
1313

14-
register(Card, "plotly-graph", ["config", "hass"]);
14+
register(App, "plotly-graph", ["config", "hass"]);
1515

1616
console.info(
1717
`%c PLOTLY-GRAPH-CARD %c ${version} `,

0 commit comments

Comments
 (0)