preact-homeassistant 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +38 -22
- package/dist/index.js +181 -140
- package/dist/index.js.map +1 -1
- package/package.json +32 -15
- package/src/HAContext.tsx +162 -120
- package/src/__tests__/cacheUtils.test.ts +23 -48
- package/src/__tests__/registerPreactCard.test.tsx +75 -0
- package/src/__tests__/testHelpers.tsx +11 -1
- package/src/__tests__/useCachedFetch.test.tsx +57 -33
- package/src/__tests__/{useMultiCalendarEvents.test.tsx → useCalendarEvents.test.tsx} +3 -4
- package/src/__tests__/useEntity.test.tsx +11 -17
- package/src/__tests__/useHassValue.test.tsx +99 -0
- package/src/cacheUtils.ts +12 -24
- package/src/index.ts +3 -2
- package/src/registerPreactCard.tsx +101 -32
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { HassServices } from 'home-assistant-js-websocket';
|
|
|
10
10
|
import { JSX } from 'preact';
|
|
11
11
|
import { RefObject } from 'preact';
|
|
12
12
|
|
|
13
|
+
declare type Cache_2 = Map<string, CacheEntry<unknown>>;
|
|
14
|
+
|
|
15
|
+
declare interface CacheEntry<T> {
|
|
16
|
+
data: T;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Calendar entity - only exposes the current/next event.
|
|
15
21
|
* Use useCalendarEvents() to fetch a list of events.
|
|
@@ -147,11 +153,13 @@ export declare type ForecastType = 'daily' | 'hourly' | 'twice_daily';
|
|
|
147
153
|
*/
|
|
148
154
|
export declare function getAllStyles(): string;
|
|
149
155
|
|
|
150
|
-
export declare function HAProvider({ hass, subscribeToEntity, children }: HAProviderProps): JSX.Element;
|
|
156
|
+
export declare function HAProvider({ hass, subscribeToEntity, subscribeToHass, cache, children, }: HAProviderProps): JSX.Element;
|
|
151
157
|
|
|
152
158
|
declare interface HAProviderProps {
|
|
153
159
|
hass: HomeAssistant | undefined;
|
|
154
160
|
subscribeToEntity: (entityId: string, callback: (entity: any) => void) => () => void;
|
|
161
|
+
subscribeToHass?: SubscribeToHass;
|
|
162
|
+
cache?: Cache_2;
|
|
155
163
|
children: ComponentChildren;
|
|
156
164
|
}
|
|
157
165
|
|
|
@@ -174,8 +182,6 @@ declare type KnownDomain = keyof DomainEntityMap;
|
|
|
174
182
|
|
|
175
183
|
declare type KnownServiceDomain = keyof DomainServiceMap;
|
|
176
184
|
|
|
177
|
-
export declare function loadFromCache<T>(key: string): T | undefined;
|
|
178
|
-
|
|
179
185
|
export declare function registerPreactCard<TConfig>(options: RegisterPreactCardOptions<TConfig>): void;
|
|
180
186
|
|
|
181
187
|
declare interface RegisterPreactCardOptions<TConfig> {
|
|
@@ -202,8 +208,6 @@ export declare function registerRawStyles(styles: string): void;
|
|
|
202
208
|
|
|
203
209
|
export declare type ResizeCallback = (size: ElementSize) => void;
|
|
204
210
|
|
|
205
|
-
export declare function saveToCache<T>(key: string, data: T): void;
|
|
206
|
-
|
|
207
211
|
declare type ServiceCaller<T extends string> = <S extends keyof ServicesForId<T> & string>(service: S, ...args: ServicesForId<T>[S] extends undefined ? [] : Record<string, never> extends Exclude<ServicesForId<T>[S], undefined> ? [data?: ServicesForId<T>[S]] : [data: ServicesForId<T>[S]]) => Promise<void>;
|
|
208
212
|
|
|
209
213
|
/**
|
|
@@ -214,6 +218,8 @@ declare type ServiceCaller<T extends string> = <S extends keyof ServicesForId<T>
|
|
|
214
218
|
*/
|
|
215
219
|
export declare type ServicesForId<T extends string> = T extends `${infer D}.${string}` ? D extends KnownServiceDomain ? DomainServiceMap[D] : Record<string, Record<string, unknown> | undefined> : Record<string, Record<string, unknown> | undefined>;
|
|
216
220
|
|
|
221
|
+
declare type SubscribeToHass = (callback: () => void) => () => void;
|
|
222
|
+
|
|
217
223
|
/**
|
|
218
224
|
* Sun entity - provides sunrise/sunset and elevation information.
|
|
219
225
|
*/
|
|
@@ -247,18 +253,29 @@ declare interface UseCachedFetchResult<T> {
|
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
/**
|
|
250
|
-
* Fetch
|
|
256
|
+
* Fetch events from one or more calendars for a date range, with in-memory
|
|
257
|
+
* (per-card) caching and stale-while-revalidate behavior. Events are tagged
|
|
258
|
+
* with their source calendar ID. Returns `prefetch` to warm adjacent ranges.
|
|
251
259
|
*/
|
|
252
|
-
export declare function useCalendarEvents(
|
|
260
|
+
export declare function useCalendarEvents(entityIds: `calendar.${string}`[], options: {
|
|
253
261
|
start: Date;
|
|
254
262
|
end: Date;
|
|
255
263
|
}): UseCalendarEventsResult;
|
|
256
264
|
|
|
257
265
|
declare interface UseCalendarEventsResult {
|
|
258
|
-
events:
|
|
259
|
-
|
|
266
|
+
events: CalendarEventWithSource[] | undefined;
|
|
267
|
+
status: FetchStatus;
|
|
260
268
|
error: Error | undefined;
|
|
261
269
|
refetch: () => void;
|
|
270
|
+
/**
|
|
271
|
+
* Warm the cache for an arbitrary range (e.g. adjacent months) without
|
|
272
|
+
* touching component state. Best-effort: skips ranges already cached and
|
|
273
|
+
* swallows failures.
|
|
274
|
+
*/
|
|
275
|
+
prefetch: (range: {
|
|
276
|
+
start: Date;
|
|
277
|
+
end: Date;
|
|
278
|
+
}) => void;
|
|
262
279
|
}
|
|
263
280
|
|
|
264
281
|
/**
|
|
@@ -270,6 +287,9 @@ declare interface UseCalendarEventsResult {
|
|
|
270
287
|
*/
|
|
271
288
|
export declare function useCallbackStable<T extends (...args: never[]) => unknown>(callback: T): T;
|
|
272
289
|
|
|
290
|
+
/** Re-renders when the active theme's dark mode flips. */
|
|
291
|
+
export declare function useDarkMode(): boolean;
|
|
292
|
+
|
|
273
293
|
/**
|
|
274
294
|
* Subscribe to a specific entity by ID. Re-renders only when that entity changes.
|
|
275
295
|
*
|
|
@@ -289,21 +309,17 @@ export declare function useHass(): {
|
|
|
289
309
|
getHass: () => HomeAssistant | undefined;
|
|
290
310
|
};
|
|
291
311
|
|
|
312
|
+
/** Re-renders when `hass.config` changes (units, latitude/longitude, etc.). */
|
|
313
|
+
export declare function useHassConfig(): HomeAssistant['config'] | undefined;
|
|
314
|
+
|
|
292
315
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
316
|
+
* Subscribe to a derived slice of the `hass` object (e.g. config, themes) and
|
|
317
|
+
* re-render only when that slice changes. Use this for non-entity values —
|
|
318
|
+
* entity state goes through `useEntity`. The selector runs on every hass update
|
|
319
|
+
* but only re-renders the consumer when `isEqual` reports a change, so it's
|
|
320
|
+
* cheap for rarely-changing values like config/themes.
|
|
295
321
|
*/
|
|
296
|
-
export declare function
|
|
297
|
-
start: Date;
|
|
298
|
-
end: Date;
|
|
299
|
-
}): UseMultiCalendarEventsResult;
|
|
300
|
-
|
|
301
|
-
declare interface UseMultiCalendarEventsResult {
|
|
302
|
-
events: CalendarEventWithSource[] | undefined;
|
|
303
|
-
status: FetchStatus;
|
|
304
|
-
error: Error | undefined;
|
|
305
|
-
refetch: () => void;
|
|
306
|
-
}
|
|
322
|
+
export declare function useHassValue<T>(selector: (hass: HomeAssistant | undefined) => T, isEqual?: (a: T, b: T) => boolean): T;
|
|
307
323
|
|
|
308
324
|
/**
|
|
309
325
|
* Observe an element's size via ResizeObserver. The callback fires:
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
2
2
|
import { createContext, render } from "preact";
|
|
3
3
|
import { useRef, useMemo, useState, useEffect, useContext } from "preact/hooks";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function loadFromCache(key) {
|
|
7
|
-
try {
|
|
8
|
-
const raw = localStorage.getItem(`${CACHE_PREFIX}${key}`);
|
|
9
|
-
if (!raw) return void 0;
|
|
10
|
-
const entry = JSON.parse(raw);
|
|
11
|
-
if (Date.now() - entry.timestamp > CACHE_EXPIRY_MS) {
|
|
12
|
-
localStorage.removeItem(`${CACHE_PREFIX}${key}`);
|
|
13
|
-
return void 0;
|
|
14
|
-
}
|
|
15
|
-
return entry.data;
|
|
16
|
-
} catch {
|
|
17
|
-
return void 0;
|
|
18
|
-
}
|
|
4
|
+
function readCache(cache, key) {
|
|
5
|
+
return cache.get(key)?.data;
|
|
19
6
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
const entry = { data, timestamp: Date.now() };
|
|
23
|
-
localStorage.setItem(`${CACHE_PREFIX}${key}`, JSON.stringify(entry));
|
|
24
|
-
} catch (e) {
|
|
25
|
-
console.warn("[preact-homeassistant cache] Failed to save:", e);
|
|
26
|
-
}
|
|
7
|
+
function writeCache(cache, key, data) {
|
|
8
|
+
cache.set(key, { data });
|
|
27
9
|
}
|
|
28
10
|
function useCallbackStable(callback) {
|
|
29
11
|
const callbackRef = useRef(callback);
|
|
@@ -36,18 +18,30 @@ function useCallbackStable(callback) {
|
|
|
36
18
|
}
|
|
37
19
|
return stableRef.current;
|
|
38
20
|
}
|
|
21
|
+
const noopSubscribeToHass = () => () => {
|
|
22
|
+
};
|
|
39
23
|
const HAContext = createContext(null);
|
|
40
|
-
function HAProvider({
|
|
24
|
+
function HAProvider({
|
|
25
|
+
hass,
|
|
26
|
+
subscribeToEntity,
|
|
27
|
+
subscribeToHass,
|
|
28
|
+
cache,
|
|
29
|
+
children
|
|
30
|
+
}) {
|
|
41
31
|
const hassRef = useRef(hass);
|
|
42
32
|
hassRef.current = hass;
|
|
43
33
|
const getHass = useCallbackStable(() => hassRef.current);
|
|
34
|
+
const cacheRef = useRef();
|
|
35
|
+
if (!cacheRef.current) cacheRef.current = cache ?? /* @__PURE__ */ new Map();
|
|
36
|
+
const resolvedSubscribeToHass = subscribeToHass ?? noopSubscribeToHass;
|
|
44
37
|
const store = useMemo(
|
|
45
38
|
() => ({
|
|
46
|
-
hass: hassRef.current,
|
|
47
39
|
getHass,
|
|
48
|
-
subscribeToEntity
|
|
40
|
+
subscribeToEntity,
|
|
41
|
+
subscribeToHass: resolvedSubscribeToHass,
|
|
42
|
+
cache: cacheRef.current
|
|
49
43
|
}),
|
|
50
|
-
[getHass, subscribeToEntity]
|
|
44
|
+
[getHass, subscribeToEntity, resolvedSubscribeToHass]
|
|
51
45
|
);
|
|
52
46
|
return /* @__PURE__ */ jsx(HAContext.Provider, { value: store, children });
|
|
53
47
|
}
|
|
@@ -62,23 +56,45 @@ function useEntity(entityId) {
|
|
|
62
56
|
const store = useHAStore();
|
|
63
57
|
const cacheKey = `entity:${entityId}`;
|
|
64
58
|
const [entity, setEntity] = useState(() => {
|
|
65
|
-
const current = store.
|
|
59
|
+
const current = store.getHass()?.states[entityId];
|
|
66
60
|
if (current) return current;
|
|
67
|
-
return
|
|
61
|
+
return readCache(store.cache, cacheKey);
|
|
68
62
|
});
|
|
69
63
|
useEffect(() => {
|
|
70
64
|
const unsubscribe = store.subscribeToEntity(entityId, (newEntity) => {
|
|
71
65
|
setEntity(newEntity);
|
|
72
|
-
|
|
66
|
+
writeCache(store.cache, cacheKey, newEntity);
|
|
73
67
|
});
|
|
74
68
|
return unsubscribe;
|
|
75
|
-
}, [entityId, store.subscribeToEntity, cacheKey]);
|
|
69
|
+
}, [entityId, store.subscribeToEntity, store.cache, cacheKey]);
|
|
76
70
|
return entity;
|
|
77
71
|
}
|
|
78
72
|
function useHass() {
|
|
79
73
|
const store = useHAStore();
|
|
80
74
|
return { getHass: store.getHass };
|
|
81
75
|
}
|
|
76
|
+
function useHassValue(selector, isEqual = Object.is) {
|
|
77
|
+
const store = useHAStore();
|
|
78
|
+
const selectorRef = useRef(selector);
|
|
79
|
+
selectorRef.current = selector;
|
|
80
|
+
const isEqualRef = useRef(isEqual);
|
|
81
|
+
isEqualRef.current = isEqual;
|
|
82
|
+
const [value, setValue] = useState(() => selectorRef.current(store.getHass()));
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const unsubscribe = store.subscribeToHass(() => {
|
|
85
|
+
const next = selectorRef.current(store.getHass());
|
|
86
|
+
setValue((prev) => isEqualRef.current(prev, next) ? prev : next);
|
|
87
|
+
});
|
|
88
|
+
return unsubscribe;
|
|
89
|
+
}, [store.subscribeToHass, store.getHass]);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function useHassConfig() {
|
|
93
|
+
return useHassValue((hass) => hass?.config);
|
|
94
|
+
}
|
|
95
|
+
function useDarkMode() {
|
|
96
|
+
return useHassValue((hass) => hass?.themes?.darkMode ?? false);
|
|
97
|
+
}
|
|
82
98
|
function useService(entityId) {
|
|
83
99
|
const { getHass } = useHass();
|
|
84
100
|
return useCallbackStable(((service, data) => {
|
|
@@ -89,10 +105,19 @@ function useService(entityId) {
|
|
|
89
105
|
}));
|
|
90
106
|
}
|
|
91
107
|
function useCachedFetch(cacheKey, fetcher, deps) {
|
|
92
|
-
const
|
|
108
|
+
const store = useHAStore();
|
|
109
|
+
const [data, setData] = useState(() => readCache(store.cache, cacheKey));
|
|
93
110
|
const [isFresh, setIsFresh] = useState(false);
|
|
94
111
|
const [isFetching, setIsFetching] = useState(false);
|
|
95
112
|
const [error, setError] = useState(void 0);
|
|
113
|
+
const dataKeyRef = useRef(cacheKey);
|
|
114
|
+
if (cacheKey !== dataKeyRef.current) {
|
|
115
|
+
dataKeyRef.current = cacheKey;
|
|
116
|
+
setIsFresh(false);
|
|
117
|
+
setError(void 0);
|
|
118
|
+
const cached = readCache(store.cache, cacheKey);
|
|
119
|
+
if (cached !== void 0) setData(cached);
|
|
120
|
+
}
|
|
96
121
|
const fetchIdRef = useRef(0);
|
|
97
122
|
const doFetch = useCallbackStable(async () => {
|
|
98
123
|
const fetchId = ++fetchIdRef.current;
|
|
@@ -103,7 +128,7 @@ function useCachedFetch(cacheKey, fetcher, deps) {
|
|
|
103
128
|
if (fetchId === fetchIdRef.current) {
|
|
104
129
|
setData(result);
|
|
105
130
|
setIsFresh(true);
|
|
106
|
-
|
|
131
|
+
writeCache(store.cache, cacheKey, result);
|
|
107
132
|
}
|
|
108
133
|
} catch (err) {
|
|
109
134
|
if (fetchId === fetchIdRef.current) {
|
|
@@ -126,97 +151,62 @@ function useCachedFetch(cacheKey, fetcher, deps) {
|
|
|
126
151
|
}, [data, isFresh, isFetching]);
|
|
127
152
|
return { data, status, error, refetch: doFetch };
|
|
128
153
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
} catch (err) {
|
|
162
|
-
if (fetchId === fetchIdRef.current) {
|
|
163
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
164
|
-
setLoading(false);
|
|
154
|
+
function calendarEventsCacheKey(entityIds, range) {
|
|
155
|
+
return `events:${entityIds.join(",")}:${range.start.getTime()}-${range.end.getTime()}`;
|
|
156
|
+
}
|
|
157
|
+
async function fetchCalendarRange(hass, entityIds, range) {
|
|
158
|
+
if (!hass?.connection) {
|
|
159
|
+
throw new Error("Home Assistant connection not available");
|
|
160
|
+
}
|
|
161
|
+
if (entityIds.length === 0) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const results = await Promise.all(
|
|
165
|
+
entityIds.map(async (entityId) => {
|
|
166
|
+
try {
|
|
167
|
+
const result = await hass.connection.sendMessagePromise({
|
|
168
|
+
type: "call_service",
|
|
169
|
+
domain: "calendar",
|
|
170
|
+
service: "get_events",
|
|
171
|
+
service_data: {
|
|
172
|
+
start_date_time: range.start.toISOString(),
|
|
173
|
+
end_date_time: range.end.toISOString()
|
|
174
|
+
},
|
|
175
|
+
target: { entity_id: entityId },
|
|
176
|
+
return_response: true
|
|
177
|
+
});
|
|
178
|
+
const calendarEvents = result.response?.[entityId]?.events ?? [];
|
|
179
|
+
return calendarEvents.map(
|
|
180
|
+
(event) => ({ ...event, calendarId: entityId })
|
|
181
|
+
);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error(`Failed to fetch events for ${entityId}:`, err);
|
|
184
|
+
return [];
|
|
165
185
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
fetchEvents();
|
|
170
|
-
}, [entityId, options.start.getTime(), options.end.getTime(), fetchEvents]);
|
|
171
|
-
return { events, loading, error, refetch: fetchEvents };
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
return results.flat();
|
|
172
189
|
}
|
|
173
|
-
function
|
|
190
|
+
function useCalendarEvents(entityIds, options) {
|
|
174
191
|
const store = useHAStore();
|
|
175
192
|
const { getHass } = useHass();
|
|
176
193
|
const debounceTimerRef = useRef(null);
|
|
177
194
|
const entityIdsKey = entityIds.join(",");
|
|
178
195
|
const dateRangeKey = `${options.start.getTime()}-${options.end.getTime()}`;
|
|
179
196
|
const cacheKey = `events:${entityIdsKey}:${dateRangeKey}`;
|
|
180
|
-
const fetcher = useCallbackStable(
|
|
181
|
-
const hass = getHass();
|
|
182
|
-
if (!hass?.connection) {
|
|
183
|
-
throw new Error("Home Assistant connection not available");
|
|
184
|
-
}
|
|
185
|
-
if (entityIds.length === 0) {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
const results = await Promise.all(
|
|
189
|
-
entityIds.map(async (entityId) => {
|
|
190
|
-
try {
|
|
191
|
-
const result = await hass.connection.sendMessagePromise({
|
|
192
|
-
type: "call_service",
|
|
193
|
-
domain: "calendar",
|
|
194
|
-
service: "get_events",
|
|
195
|
-
service_data: {
|
|
196
|
-
start_date_time: options.start.toISOString(),
|
|
197
|
-
end_date_time: options.end.toISOString()
|
|
198
|
-
},
|
|
199
|
-
target: { entity_id: entityId },
|
|
200
|
-
return_response: true
|
|
201
|
-
});
|
|
202
|
-
const calendarEvents = result.response?.[entityId]?.events ?? [];
|
|
203
|
-
return calendarEvents.map(
|
|
204
|
-
(event) => ({ ...event, calendarId: entityId })
|
|
205
|
-
);
|
|
206
|
-
} catch (err) {
|
|
207
|
-
console.error(`Failed to fetch events for ${entityId}:`, err);
|
|
208
|
-
return [];
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
);
|
|
212
|
-
return results.flat();
|
|
213
|
-
});
|
|
197
|
+
const fetcher = useCallbackStable(() => fetchCalendarRange(getHass(), entityIds, options));
|
|
214
198
|
const {
|
|
215
199
|
data: events,
|
|
216
200
|
status,
|
|
217
201
|
error,
|
|
218
202
|
refetch
|
|
219
203
|
} = useCachedFetch(cacheKey, fetcher, [entityIdsKey, dateRangeKey]);
|
|
204
|
+
const prefetch = useCallbackStable((range) => {
|
|
205
|
+
const key = calendarEventsCacheKey(entityIds, range);
|
|
206
|
+
if (store.cache.has(key)) return;
|
|
207
|
+
fetchCalendarRange(getHass(), entityIds, range).then((result) => writeCache(store.cache, key, result)).catch(() => {
|
|
208
|
+
});
|
|
209
|
+
});
|
|
220
210
|
const debouncedRefetch = useCallbackStable(() => {
|
|
221
211
|
if (debounceTimerRef.current) {
|
|
222
212
|
clearTimeout(debounceTimerRef.current);
|
|
@@ -234,7 +224,7 @@ function useMultiCalendarEvents(entityIds, options) {
|
|
|
234
224
|
}
|
|
235
225
|
};
|
|
236
226
|
}, [entityIdsKey, store.subscribeToEntity, debouncedRefetch]);
|
|
237
|
-
return { events, status, error, refetch };
|
|
227
|
+
return { events, status, error, refetch, prefetch };
|
|
238
228
|
}
|
|
239
229
|
function useWeatherForecast(entityId, type) {
|
|
240
230
|
const store = useHAStore();
|
|
@@ -313,6 +303,7 @@ function registerRawStyles(styles) {
|
|
|
313
303
|
function getAllStyles() {
|
|
314
304
|
return styleRegistry.join("\n");
|
|
315
305
|
}
|
|
306
|
+
const TEARDOWN_GRACE_MS = 5e3;
|
|
316
307
|
function registerPreactCard(options) {
|
|
317
308
|
const {
|
|
318
309
|
type,
|
|
@@ -323,19 +314,55 @@ function registerPreactCard(options) {
|
|
|
323
314
|
UnconfiguredComponent,
|
|
324
315
|
getStubConfig
|
|
325
316
|
} = options;
|
|
326
|
-
class
|
|
317
|
+
class BaseHACard extends HTMLElement {
|
|
327
318
|
_hass;
|
|
328
319
|
_config;
|
|
320
|
+
_teardownTimer;
|
|
321
|
+
_renderUnconfigured() {
|
|
322
|
+
}
|
|
323
|
+
connectedCallback() {
|
|
324
|
+
if (this._teardownTimer !== void 0) {
|
|
325
|
+
clearTimeout(this._teardownTimer);
|
|
326
|
+
this._teardownTimer = void 0;
|
|
327
|
+
}
|
|
328
|
+
this._maybeRenderOnConnect();
|
|
329
|
+
}
|
|
330
|
+
_maybeRenderOnConnect() {
|
|
331
|
+
if (this._hass && this._config) {
|
|
332
|
+
this._render();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
disconnectedCallback() {
|
|
336
|
+
if (this._teardownTimer !== void 0) clearTimeout(this._teardownTimer);
|
|
337
|
+
this._teardownTimer = setTimeout(() => {
|
|
338
|
+
render(null, this._getRenderRoot());
|
|
339
|
+
this._teardownTimer = void 0;
|
|
340
|
+
}, TEARDOWN_GRACE_MS);
|
|
341
|
+
}
|
|
342
|
+
setConfig(config) {
|
|
343
|
+
this._config = config;
|
|
344
|
+
if (this._hass && this.isConnected) {
|
|
345
|
+
this._render();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
_render() {
|
|
349
|
+
if (!this._config || !this._hass) {
|
|
350
|
+
this._renderUnconfigured();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this._renderTree();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
class HACard extends BaseHACard {
|
|
329
357
|
_shadowRoot;
|
|
330
358
|
_entityChangeListeners = /* @__PURE__ */ new Map();
|
|
359
|
+
_hassChangeListeners = /* @__PURE__ */ new Set();
|
|
331
360
|
constructor() {
|
|
332
361
|
super();
|
|
333
362
|
this._shadowRoot = this.attachShadow({ mode: "open" });
|
|
334
363
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
this._render();
|
|
338
|
-
}
|
|
364
|
+
_getRenderRoot() {
|
|
365
|
+
return this._shadowRoot;
|
|
339
366
|
}
|
|
340
367
|
set hass(hass) {
|
|
341
368
|
const prevStates = this._hass?.states;
|
|
@@ -347,16 +374,11 @@ function registerPreactCard(options) {
|
|
|
347
374
|
listeners.forEach((listener) => listener(newState));
|
|
348
375
|
}
|
|
349
376
|
}
|
|
377
|
+
this._hassChangeListeners.forEach((listener) => listener());
|
|
350
378
|
if (!prevStates && this._config && this.isConnected) {
|
|
351
379
|
this._render();
|
|
352
380
|
}
|
|
353
381
|
}
|
|
354
|
-
setConfig(config) {
|
|
355
|
-
this._config = config;
|
|
356
|
-
if (this._hass && this.isConnected) {
|
|
357
|
-
this._render();
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
382
|
_subscribeToEntity = (entityId, callback) => {
|
|
361
383
|
if (!this._entityChangeListeners.has(entityId)) {
|
|
362
384
|
this._entityChangeListeners.set(entityId, /* @__PURE__ */ new Set());
|
|
@@ -372,21 +394,34 @@ function registerPreactCard(options) {
|
|
|
372
394
|
}
|
|
373
395
|
};
|
|
374
396
|
};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
397
|
+
_subscribeToHass = (callback) => {
|
|
398
|
+
this._hassChangeListeners.add(callback);
|
|
399
|
+
return () => {
|
|
400
|
+
this._hassChangeListeners.delete(callback);
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
_renderTree() {
|
|
382
404
|
render(
|
|
383
|
-
/* @__PURE__ */ jsxs(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
405
|
+
/* @__PURE__ */ jsxs(
|
|
406
|
+
HAProvider,
|
|
407
|
+
{
|
|
408
|
+
hass: this._hass,
|
|
409
|
+
subscribeToEntity: this._subscribeToEntity,
|
|
410
|
+
subscribeToHass: this._subscribeToHass,
|
|
411
|
+
children: [
|
|
412
|
+
/* @__PURE__ */ jsx("style", { children: getAllStyles() }),
|
|
413
|
+
/* @__PURE__ */ jsx(Component, { config: this._config })
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
),
|
|
387
417
|
this._shadowRoot
|
|
388
418
|
);
|
|
389
419
|
}
|
|
420
|
+
_renderUnconfigured() {
|
|
421
|
+
if (UnconfiguredComponent) {
|
|
422
|
+
render(/* @__PURE__ */ jsx(UnconfiguredComponent, {}), this._shadowRoot);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
390
425
|
static getConfigElement() {
|
|
391
426
|
if (ConfigComponent) {
|
|
392
427
|
return document.createElement(`${type}-editor`);
|
|
@@ -400,13 +435,19 @@ function registerPreactCard(options) {
|
|
|
400
435
|
customElements.define(type, HACard);
|
|
401
436
|
if (ConfigComponent) {
|
|
402
437
|
const EditorComponent = ConfigComponent;
|
|
403
|
-
class HACardEditor extends
|
|
404
|
-
|
|
405
|
-
|
|
438
|
+
class HACardEditor extends BaseHACard {
|
|
439
|
+
_getRenderRoot() {
|
|
440
|
+
return this;
|
|
441
|
+
}
|
|
442
|
+
// The editor re-renders on every hass update: it passes `hass` straight to
|
|
443
|
+
// HA's <ha-form>/<ha-selector>, whose entity pickers need a fresh hass to
|
|
444
|
+
// stay current. (Unlike the card, which renders once then subscribes.)
|
|
406
445
|
set hass(hass) {
|
|
407
446
|
this._hass = hass;
|
|
408
447
|
this._render();
|
|
409
448
|
}
|
|
449
|
+
// Render whenever config arrives, regardless of connection — HA may set
|
|
450
|
+
// config/hass before connecting the editor element.
|
|
410
451
|
setConfig(config) {
|
|
411
452
|
this._config = config;
|
|
412
453
|
this._render();
|
|
@@ -420,8 +461,8 @@ function registerPreactCard(options) {
|
|
|
420
461
|
})
|
|
421
462
|
);
|
|
422
463
|
};
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
// Render to light DOM so HA's custom elements (ha-form etc.) work.
|
|
465
|
+
_renderTree() {
|
|
425
466
|
render(
|
|
426
467
|
/* @__PURE__ */ jsx(
|
|
427
468
|
EditorComponent,
|
|
@@ -488,16 +529,16 @@ export {
|
|
|
488
529
|
HAProvider,
|
|
489
530
|
css,
|
|
490
531
|
getAllStyles,
|
|
491
|
-
loadFromCache,
|
|
492
532
|
registerPreactCard,
|
|
493
533
|
registerRawStyles,
|
|
494
|
-
saveToCache,
|
|
495
534
|
useCachedFetch,
|
|
496
535
|
useCalendarEvents,
|
|
497
536
|
useCallbackStable,
|
|
537
|
+
useDarkMode,
|
|
498
538
|
useEntity,
|
|
499
539
|
useHass,
|
|
500
|
-
|
|
540
|
+
useHassConfig,
|
|
541
|
+
useHassValue,
|
|
501
542
|
useResizeObserver,
|
|
502
543
|
useService,
|
|
503
544
|
useWeatherForecast,
|