preact-homeassistant 0.3.0 → 0.5.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/README.md +30 -3
- package/dist/index.d.ts +49 -1
- package/dist/index.js +157 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/HACard.tsx +58 -0
- package/src/HAContext.tsx +2 -125
- package/src/__tests__/calendarMutations.test.ts +81 -0
- package/src/__tests__/useCalendarEvents.test.tsx +85 -1
- package/src/calendars.ts +251 -0
- package/src/index.ts +8 -1
- package/src/types/calendar.ts +3 -1
- package/src/types/common.ts +10 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ pnpm add preact preact-homeassistant
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
17
17
|
```tsx
|
|
18
|
-
import { registerPreactCard, useEntity, css } from 'preact-homeassistant';
|
|
18
|
+
import { registerPreactCard, HACard, useEntity, css } from 'preact-homeassistant';
|
|
19
19
|
|
|
20
20
|
css`
|
|
21
21
|
.my-card { padding: 16px; }
|
|
@@ -26,11 +26,11 @@ function MyCardContent({ config }: { config: { entity: string } }) {
|
|
|
26
26
|
const weather = useEntity(config.entity);
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<
|
|
29
|
+
<HACard>
|
|
30
30
|
<div class="card-content my-card">
|
|
31
31
|
<span class="temperature">{weather?.state ?? '...'}</span>
|
|
32
32
|
</div>
|
|
33
|
-
</
|
|
33
|
+
</HACard>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -87,6 +87,33 @@ prop and uses hooks for everything else.
|
|
|
87
87
|
The card renders into a Shadow DOM root. The editor renders into the light DOM
|
|
88
88
|
(required for HA's own custom elements like `<ha-select>` to work).
|
|
89
89
|
|
|
90
|
+
## `<HACard>`
|
|
91
|
+
|
|
92
|
+
Use `HACard` as the root of your card instead of a raw `<ha-card>`. It makes the
|
|
93
|
+
card fill the height Home Assistant assigns it.
|
|
94
|
+
|
|
95
|
+
In HA's **sections (grid)** layout, when a card is resized (e.g. to 3 rows) HA
|
|
96
|
+
gives the card's host element a definite height. A plain `<ha-card>` collapses to
|
|
97
|
+
its natural content height and renders slightly short, leaving a gap. `HACard`
|
|
98
|
+
sets the host and the `ha-card` to fill that height, so the card matches the slot
|
|
99
|
+
exactly. In layouts with no fixed height (masonry, auto rows) it safely collapses
|
|
100
|
+
back to natural height, so it's a drop-in replacement everywhere.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<HACard class="size-large" align="space-between">
|
|
104
|
+
<div class="card-content my-card">…</div>
|
|
105
|
+
</HACard>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | Description |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `align` | `HACardAlign` | `'top'` | How content is distributed vertically when the slot is taller than the content. Friendly aliases `top` / `center` / `bottom`, or any flex `justify-content` value (`space-between`, `space-around`, `space-evenly`, `flex-start`, `flex-end`). |
|
|
111
|
+
| `class` | `string` | — | Class applied to the underlying `ha-card`. |
|
|
112
|
+
| `children` | `ComponentChildren` | — | Card contents. |
|
|
113
|
+
|
|
114
|
+
`align` only positions content as a block. To make an inner section *stretch* to
|
|
115
|
+
absorb the extra height, give it `flex: 1` in your card's CSS.
|
|
116
|
+
|
|
90
117
|
## Hooks
|
|
91
118
|
|
|
92
119
|
### `useEntity(entityId)`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ComponentChildren } from 'preact';
|
|
2
2
|
import { ComponentType } from 'preact';
|
|
3
3
|
import { Connection } from 'home-assistant-js-websocket';
|
|
4
|
+
import { createElement } from 'preact';
|
|
4
5
|
import { HassConfig } from 'home-assistant-js-websocket';
|
|
5
6
|
import { HassEntities } from 'home-assistant-js-websocket';
|
|
6
7
|
import { HassEntity } from 'home-assistant-js-websocket';
|
|
@@ -34,7 +35,9 @@ export declare interface CalendarEntity extends HassEntityBase {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* Calendar event
|
|
38
|
+
* Calendar event as normalized by useCalendarEvents. uid/recurrence_id/rrule
|
|
39
|
+
* are only present when fetched via the REST API (GET /api/calendars/…) —
|
|
40
|
+
* the calendar.get_events service response omits them.
|
|
38
41
|
*/
|
|
39
42
|
export declare interface CalendarEvent {
|
|
40
43
|
start: string;
|
|
@@ -55,12 +58,38 @@ export declare interface CalendarEventWithSource extends CalendarEvent {
|
|
|
55
58
|
calendarId: string;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Event payload for the calendar mutation WebSocket commands. Dates are either
|
|
63
|
+
* date-only strings ("2026-07-17", all-day) or ISO datetimes.
|
|
64
|
+
*/
|
|
65
|
+
export declare interface CalendarMutationEvent {
|
|
66
|
+
dtstart: string;
|
|
67
|
+
dtend: string;
|
|
68
|
+
summary: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
location?: string;
|
|
71
|
+
rrule?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create an event on a calendar that supports mutation (e.g. Local Calendar).
|
|
76
|
+
* Requires entity control permission, not admin. WS errors reject unchanged so
|
|
77
|
+
* callers can inspect `err.code` (e.g. 'unauthorized').
|
|
78
|
+
*/
|
|
79
|
+
export declare function createCalendarEvent(hass: HomeAssistant | undefined, entityId: `calendar.${string}`, event: CalendarMutationEvent): Promise<void>;
|
|
80
|
+
|
|
58
81
|
/**
|
|
59
82
|
* CSS tagged template literal for syntax highlighting.
|
|
60
83
|
* Automatically registers the styles with the global registry.
|
|
61
84
|
*/
|
|
62
85
|
export declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => string;
|
|
63
86
|
|
|
87
|
+
/** Delete a calendar event by its uid. */
|
|
88
|
+
export declare function deleteCalendarEvent(hass: HomeAssistant | undefined, entityId: `calendar.${string}`, uid: string, opts?: {
|
|
89
|
+
recurrenceId?: string;
|
|
90
|
+
recurrenceRange?: string;
|
|
91
|
+
}): Promise<void>;
|
|
92
|
+
|
|
64
93
|
/**
|
|
65
94
|
* Map of known HA domains to their strict entity types. Contributors adding
|
|
66
95
|
* new domain types should add a new file under `src/types/` and extend this map.
|
|
@@ -153,6 +182,16 @@ export declare type ForecastType = 'daily' | 'hourly' | 'twice_daily';
|
|
|
153
182
|
*/
|
|
154
183
|
export declare function getAllStyles(): string;
|
|
155
184
|
|
|
185
|
+
export declare function HACard({ align, class: className, children }: HACardProps): createElement.JSX.Element;
|
|
186
|
+
|
|
187
|
+
export declare type HACardAlign = 'top' | 'center' | 'bottom' | 'flex-start' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly';
|
|
188
|
+
|
|
189
|
+
declare interface HACardProps {
|
|
190
|
+
align?: HACardAlign;
|
|
191
|
+
class?: string;
|
|
192
|
+
children?: ComponentChildren;
|
|
193
|
+
}
|
|
194
|
+
|
|
156
195
|
export declare function HAProvider({ hass, subscribeToEntity, subscribeToHass, cache, children, }: HAProviderProps): JSX.Element;
|
|
157
196
|
|
|
158
197
|
declare interface HAProviderProps {
|
|
@@ -172,6 +211,12 @@ export declare interface HomeAssistant {
|
|
|
172
211
|
services: HassServices;
|
|
173
212
|
connection: Connection;
|
|
174
213
|
callService: (domain: string, service: string, data?: object) => Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* Authenticated REST call against HA (`path` has no leading `api/`).
|
|
216
|
+
* Present on the runtime hass object; optional here so test mocks that
|
|
217
|
+
* don't need REST access keep compiling.
|
|
218
|
+
*/
|
|
219
|
+
callApi?: <T = unknown>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, data?: object) => Promise<T>;
|
|
175
220
|
themes?: {
|
|
176
221
|
darkMode?: boolean;
|
|
177
222
|
theme?: string;
|
|
@@ -239,6 +284,9 @@ export declare interface SunEntity extends HassEntityBase {
|
|
|
239
284
|
};
|
|
240
285
|
}
|
|
241
286
|
|
|
287
|
+
/** Replace a calendar event's content by its uid. */
|
|
288
|
+
export declare function updateCalendarEvent(hass: HomeAssistant | undefined, entityId: `calendar.${string}`, uid: string, event: CalendarMutationEvent): Promise<void>;
|
|
289
|
+
|
|
242
290
|
/**
|
|
243
291
|
* Generic hook for fetching data with localStorage caching. Returns a cache-aware
|
|
244
292
|
* status string to distinguish cached vs fresh data.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
2
|
-
import { createContext, render } from "preact";
|
|
1
|
+
import { jsx, jsxs, Fragment } from "preact/jsx-runtime";
|
|
2
|
+
import { createContext, render, createElement } from "preact";
|
|
3
3
|
import { useRef, useMemo, useState, useEffect, useContext } from "preact/hooks";
|
|
4
4
|
function readCache(cache, key) {
|
|
5
5
|
return cache.get(key)?.data;
|
|
@@ -151,81 +151,6 @@ function useCachedFetch(cacheKey, fetcher, deps) {
|
|
|
151
151
|
}, [data, isFresh, isFetching]);
|
|
152
152
|
return { data, status, error, refetch: doFetch };
|
|
153
153
|
}
|
|
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 [];
|
|
185
|
-
}
|
|
186
|
-
})
|
|
187
|
-
);
|
|
188
|
-
return results.flat();
|
|
189
|
-
}
|
|
190
|
-
function useCalendarEvents(entityIds, options) {
|
|
191
|
-
const store = useHAStore();
|
|
192
|
-
const { getHass } = useHass();
|
|
193
|
-
const debounceTimerRef = useRef(null);
|
|
194
|
-
const entityIdsKey = entityIds.join(",");
|
|
195
|
-
const dateRangeKey = `${options.start.getTime()}-${options.end.getTime()}`;
|
|
196
|
-
const cacheKey = `events:${entityIdsKey}:${dateRangeKey}`;
|
|
197
|
-
const fetcher = useCallbackStable(() => fetchCalendarRange(getHass(), entityIds, options));
|
|
198
|
-
const {
|
|
199
|
-
data: events,
|
|
200
|
-
status,
|
|
201
|
-
error,
|
|
202
|
-
refetch
|
|
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
|
-
});
|
|
210
|
-
const debouncedRefetch = useCallbackStable(() => {
|
|
211
|
-
if (debounceTimerRef.current) {
|
|
212
|
-
clearTimeout(debounceTimerRef.current);
|
|
213
|
-
}
|
|
214
|
-
debounceTimerRef.current = setTimeout(() => refetch(), 500);
|
|
215
|
-
});
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
const unsubscribes = entityIds.map(
|
|
218
|
-
(entityId) => store.subscribeToEntity(entityId, debouncedRefetch)
|
|
219
|
-
);
|
|
220
|
-
return () => {
|
|
221
|
-
unsubscribes.forEach((unsub) => unsub());
|
|
222
|
-
if (debounceTimerRef.current) {
|
|
223
|
-
clearTimeout(debounceTimerRef.current);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
}, [entityIdsKey, store.subscribeToEntity, debouncedRefetch]);
|
|
227
|
-
return { events, status, error, refetch, prefetch };
|
|
228
|
-
}
|
|
229
154
|
function useWeatherForecast(entityId, type) {
|
|
230
155
|
const store = useHAStore();
|
|
231
156
|
const { getHass } = useHass();
|
|
@@ -353,7 +278,7 @@ function registerPreactCard(options) {
|
|
|
353
278
|
this._renderTree();
|
|
354
279
|
}
|
|
355
280
|
}
|
|
356
|
-
class
|
|
281
|
+
class HACard2 extends BaseHACard {
|
|
357
282
|
_shadowRoot;
|
|
358
283
|
_entityChangeListeners = /* @__PURE__ */ new Map();
|
|
359
284
|
_hassChangeListeners = /* @__PURE__ */ new Set();
|
|
@@ -432,7 +357,7 @@ function registerPreactCard(options) {
|
|
|
432
357
|
return getStubConfig?.() ?? {};
|
|
433
358
|
}
|
|
434
359
|
}
|
|
435
|
-
customElements.define(type,
|
|
360
|
+
customElements.define(type, HACard2);
|
|
436
361
|
if (ConfigComponent) {
|
|
437
362
|
const EditorComponent = ConfigComponent;
|
|
438
363
|
class HACardEditor extends BaseHACard {
|
|
@@ -486,6 +411,155 @@ function registerPreactCard(options) {
|
|
|
486
411
|
""
|
|
487
412
|
);
|
|
488
413
|
}
|
|
414
|
+
const ALIGN_ALIASES = { top: "flex-start", bottom: "flex-end" };
|
|
415
|
+
function HACard({ align = "top", class: className, children }) {
|
|
416
|
+
const justify = ALIGN_ALIASES[align] ?? align;
|
|
417
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
418
|
+
/* @__PURE__ */ jsx("style", { children: ":host{display:block;height:100%;box-sizing:border-box;}" }),
|
|
419
|
+
createElement(
|
|
420
|
+
"ha-card",
|
|
421
|
+
{
|
|
422
|
+
class: className,
|
|
423
|
+
style: {
|
|
424
|
+
height: "100%",
|
|
425
|
+
boxSizing: "border-box",
|
|
426
|
+
display: "flex",
|
|
427
|
+
flexDirection: "column",
|
|
428
|
+
justifyContent: justify
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
children
|
|
432
|
+
)
|
|
433
|
+
] });
|
|
434
|
+
}
|
|
435
|
+
function calendarEventsCacheKey(entityIds, range) {
|
|
436
|
+
return `events:${entityIds.join(",")}:${range.start.getTime()}-${range.end.getTime()}`;
|
|
437
|
+
}
|
|
438
|
+
function apiDateString(value) {
|
|
439
|
+
return value.dateTime ?? value.date ?? "";
|
|
440
|
+
}
|
|
441
|
+
async function fetchEntityEventsRest(hass, entityId, range) {
|
|
442
|
+
const query = `start=${encodeURIComponent(range.start.toISOString())}&end=${encodeURIComponent(range.end.toISOString())}`;
|
|
443
|
+
const apiEvents = await hass.callApi(
|
|
444
|
+
"GET",
|
|
445
|
+
`calendars/${entityId}?${query}`
|
|
446
|
+
);
|
|
447
|
+
return apiEvents.map((event) => ({
|
|
448
|
+
start: apiDateString(event.start),
|
|
449
|
+
end: apiDateString(event.end),
|
|
450
|
+
summary: event.summary,
|
|
451
|
+
...event.description != null && { description: event.description },
|
|
452
|
+
...event.location != null && { location: event.location },
|
|
453
|
+
...event.uid != null && { uid: event.uid },
|
|
454
|
+
...event.recurrence_id != null && { recurrence_id: event.recurrence_id },
|
|
455
|
+
...event.rrule != null && { rrule: event.rrule }
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
async function fetchEntityEventsWs(hass, entityId, range) {
|
|
459
|
+
const result = await hass.connection.sendMessagePromise({
|
|
460
|
+
type: "call_service",
|
|
461
|
+
domain: "calendar",
|
|
462
|
+
service: "get_events",
|
|
463
|
+
service_data: {
|
|
464
|
+
start_date_time: range.start.toISOString(),
|
|
465
|
+
end_date_time: range.end.toISOString()
|
|
466
|
+
},
|
|
467
|
+
target: { entity_id: entityId },
|
|
468
|
+
return_response: true
|
|
469
|
+
});
|
|
470
|
+
return result.response?.[entityId]?.events ?? [];
|
|
471
|
+
}
|
|
472
|
+
async function fetchCalendarRange(hass, entityIds, range) {
|
|
473
|
+
if (!hass?.connection) {
|
|
474
|
+
throw new Error("Home Assistant connection not available");
|
|
475
|
+
}
|
|
476
|
+
if (entityIds.length === 0) {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
const results = await Promise.all(
|
|
480
|
+
entityIds.map(async (entityId) => {
|
|
481
|
+
try {
|
|
482
|
+
const calendarEvents = hass.callApi ? await fetchEntityEventsRest(hass, entityId, range) : await fetchEntityEventsWs(hass, entityId, range);
|
|
483
|
+
return calendarEvents.map(
|
|
484
|
+
(event) => ({ ...event, calendarId: entityId })
|
|
485
|
+
);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.error(`Failed to fetch events for ${entityId}:`, err);
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
);
|
|
492
|
+
return results.flat();
|
|
493
|
+
}
|
|
494
|
+
function useCalendarEvents(entityIds, options) {
|
|
495
|
+
const store = useHAStore();
|
|
496
|
+
const { getHass } = useHass();
|
|
497
|
+
const debounceTimerRef = useRef(null);
|
|
498
|
+
const entityIdsKey = entityIds.join(",");
|
|
499
|
+
const dateRangeKey = `${options.start.getTime()}-${options.end.getTime()}`;
|
|
500
|
+
const cacheKey = `events:${entityIdsKey}:${dateRangeKey}`;
|
|
501
|
+
const fetcher = useCallbackStable(() => fetchCalendarRange(getHass(), entityIds, options));
|
|
502
|
+
const {
|
|
503
|
+
data: events,
|
|
504
|
+
status,
|
|
505
|
+
error,
|
|
506
|
+
refetch
|
|
507
|
+
} = useCachedFetch(cacheKey, fetcher, [entityIdsKey, dateRangeKey]);
|
|
508
|
+
const prefetch = useCallbackStable((range) => {
|
|
509
|
+
const key = calendarEventsCacheKey(entityIds, range);
|
|
510
|
+
if (store.cache.has(key)) return;
|
|
511
|
+
fetchCalendarRange(getHass(), entityIds, range).then((result) => writeCache(store.cache, key, result)).catch(() => {
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
const debouncedRefetch = useCallbackStable(() => {
|
|
515
|
+
if (debounceTimerRef.current) {
|
|
516
|
+
clearTimeout(debounceTimerRef.current);
|
|
517
|
+
}
|
|
518
|
+
debounceTimerRef.current = setTimeout(() => refetch(), 500);
|
|
519
|
+
});
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
const unsubscribes = entityIds.map(
|
|
522
|
+
(entityId) => store.subscribeToEntity(entityId, debouncedRefetch)
|
|
523
|
+
);
|
|
524
|
+
return () => {
|
|
525
|
+
unsubscribes.forEach((unsub) => unsub());
|
|
526
|
+
if (debounceTimerRef.current) {
|
|
527
|
+
clearTimeout(debounceTimerRef.current);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}, [entityIdsKey, store.subscribeToEntity, debouncedRefetch]);
|
|
531
|
+
return { events, status, error, refetch, prefetch };
|
|
532
|
+
}
|
|
533
|
+
function requireConnection(hass) {
|
|
534
|
+
if (!hass?.connection) {
|
|
535
|
+
throw new Error("Home Assistant connection not available");
|
|
536
|
+
}
|
|
537
|
+
return hass;
|
|
538
|
+
}
|
|
539
|
+
async function createCalendarEvent(hass, entityId, event) {
|
|
540
|
+
await requireConnection(hass).connection.sendMessagePromise({
|
|
541
|
+
type: "calendar/event/create",
|
|
542
|
+
entity_id: entityId,
|
|
543
|
+
event
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async function deleteCalendarEvent(hass, entityId, uid, opts) {
|
|
547
|
+
await requireConnection(hass).connection.sendMessagePromise({
|
|
548
|
+
type: "calendar/event/delete",
|
|
549
|
+
entity_id: entityId,
|
|
550
|
+
uid,
|
|
551
|
+
...opts?.recurrenceId !== void 0 && { recurrence_id: opts.recurrenceId },
|
|
552
|
+
...opts?.recurrenceRange !== void 0 && { recurrence_range: opts.recurrenceRange }
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
async function updateCalendarEvent(hass, entityId, uid, event) {
|
|
556
|
+
await requireConnection(hass).connection.sendMessagePromise({
|
|
557
|
+
type: "calendar/event/update",
|
|
558
|
+
entity_id: entityId,
|
|
559
|
+
uid,
|
|
560
|
+
event
|
|
561
|
+
});
|
|
562
|
+
}
|
|
489
563
|
function useResizeObserver(ref, callback, deps = []) {
|
|
490
564
|
const callbackRef = useRef(callback);
|
|
491
565
|
callbackRef.current = callback;
|
|
@@ -526,11 +600,15 @@ function useWidth(ref) {
|
|
|
526
600
|
return width;
|
|
527
601
|
}
|
|
528
602
|
export {
|
|
603
|
+
HACard,
|
|
529
604
|
HAProvider,
|
|
605
|
+
createCalendarEvent,
|
|
530
606
|
css,
|
|
607
|
+
deleteCalendarEvent,
|
|
531
608
|
getAllStyles,
|
|
532
609
|
registerPreactCard,
|
|
533
610
|
registerRawStyles,
|
|
611
|
+
updateCalendarEvent,
|
|
534
612
|
useCachedFetch,
|
|
535
613
|
useCalendarEvents,
|
|
536
614
|
useCallbackStable,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/cacheUtils.ts","../src/useCallbackStable.ts","../src/HAContext.tsx","../src/styleRegistry.ts","../src/registerPreactCard.tsx","../src/useResizeObserver.ts","../src/useWidth.ts"],"sourcesContent":["// In-memory cache helpers. The cache Map is owned per-card by the HAProvider\n// store (see HAContext), so its lifetime matches the card and it is\n// garbage-collected when the card is torn down. There is intentionally no\n// persistence, TTL, or size cap: freshness comes from entity subscriptions and\n// periodic refetch, not from cache expiry, and growth is bounded by the ranges\n// a single card visits in a session.\n\nexport interface CacheEntry<T> {\n data: T;\n}\n\nexport type Cache = Map<string, CacheEntry<unknown>>;\n\nexport function readCache<T>(cache: Cache, key: string): T | undefined {\n return (cache.get(key) as CacheEntry<T> | undefined)?.data;\n}\n\nexport function writeCache<T>(cache: Cache, key: string, data: T): void {\n cache.set(key, { data });\n}\n","import { useRef } from 'preact/hooks';\n\n/**\n * Creates a stable callback reference that always calls the latest version of the callback.\n * Unlike useCallback, this never changes identity, so it won't cause re-renders in children.\n *\n * @param callback The callback function to stabilize\n * @returns A stable function reference that always calls the latest callback\n */\nexport function useCallbackStable<T extends (...args: never[]) => unknown>(callback: T): T {\n const callbackRef = useRef<T>(callback);\n\n callbackRef.current = callback;\n\n // Create a stable function reference once\n const stableRef = useRef<T | null>(null);\n if (stableRef.current === null) {\n stableRef.current = ((...args: Parameters<T>) => {\n return callbackRef.current(...args);\n }) as T;\n }\n\n return stableRef.current;\n}\n","import { createContext } from 'preact';\nimport type { ComponentChildren } from 'preact';\nimport { useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';\n\nimport { type Cache, readCache, writeCache } from './cacheUtils';\nimport type {\n CalendarEvent,\n CalendarEventWithSource,\n EntityForId,\n FetchStatus,\n ForecastType,\n HomeAssistant,\n ServicesForId,\n WeatherForecast,\n} from './types';\nimport { useCallbackStable } from './useCallbackStable';\n\ntype SubscribeToHass = (callback: () => void) => () => void;\n\n// Default for providers that don't wire up hass-value notifications (Storybook,\n// tests). useHassValue then simply returns its initial value and never updates.\nconst noopSubscribeToHass: SubscribeToHass = () => () => {};\n\ninterface HAStore {\n getHass: () => HomeAssistant | undefined;\n subscribeToEntity: (entityId: string, callback: (entity: any) => void) => () => void;\n subscribeToHass: SubscribeToHass;\n // Per-card cache (events, forecasts, entities). Owned by the provider so its\n // lifetime is the card's — GC'd with the store when the card is torn down.\n cache: Cache;\n}\n\nconst HAContext = createContext<HAStore | null>(null);\n\ninterface HAProviderProps {\n hass: HomeAssistant | undefined;\n subscribeToEntity: (entityId: string, callback: (entity: any) => void) => () => void;\n subscribeToHass?: SubscribeToHass;\n // Optional injected cache (tests seed/inspect it); defaults to a fresh\n // per-provider Map held stable across re-renders.\n cache?: Cache;\n children: ComponentChildren;\n}\n\nexport function HAProvider({\n hass,\n subscribeToEntity,\n subscribeToHass,\n cache,\n children,\n}: HAProviderProps) {\n const hassRef = useRef(hass);\n hassRef.current = hass;\n\n const getHass = useCallbackStable(() => hassRef.current);\n\n const cacheRef = useRef<Cache>();\n if (!cacheRef.current) cacheRef.current = cache ?? new Map();\n\n const resolvedSubscribeToHass = subscribeToHass ?? noopSubscribeToHass;\n\n const store = useMemo<HAStore>(\n () => ({\n getHass,\n subscribeToEntity,\n subscribeToHass: resolvedSubscribeToHass,\n cache: cacheRef.current!,\n }),\n [getHass, subscribeToEntity, resolvedSubscribeToHass],\n );\n\n return <HAContext.Provider value={store}>{children}</HAContext.Provider>;\n}\n\nfunction useHAStore(): HAStore {\n const store = useContext(HAContext);\n if (!store) {\n throw new Error('useEntity/useHass must be used within an HAProvider');\n }\n return store;\n}\n\n/**\n * Subscribe to a specific entity by ID. Re-renders only when that entity changes.\n *\n * Returns a typed entity based on the domain prefix:\n * - 'calendar.xyz' -> CalendarEntity\n * - 'weather.xyz' -> WeatherEntity\n * - 'sun.sun' -> SunEntity\n * - other domains -> HassEntity (fallback)\n */\nexport function useEntity<T extends string>(entityId: T): EntityForId<T> | undefined {\n const store = useHAStore();\n const cacheKey = `entity:${entityId}`;\n\n const [entity, setEntity] = useState<EntityForId<T> | undefined>(() => {\n const current = store.getHass()?.states[entityId] as EntityForId<T> | undefined;\n if (current) return current;\n return readCache<EntityForId<T>>(store.cache, cacheKey);\n });\n\n useEffect(() => {\n const unsubscribe = store.subscribeToEntity(entityId, (newEntity) => {\n setEntity(newEntity as EntityForId<T>);\n writeCache(store.cache, cacheKey, newEntity);\n });\n return unsubscribe;\n }, [entityId, store.subscribeToEntity, store.cache, cacheKey]);\n\n return entity;\n}\n\n/**\n * Get access to the full hass object for calling services / accessing config.\n * Does NOT re-render on entity changes. Use useEntity for that.\n */\nexport function useHass(): { getHass: () => HomeAssistant | undefined } {\n const store = useHAStore();\n return { getHass: store.getHass };\n}\n\n/**\n * Subscribe to a derived slice of the `hass` object (e.g. config, themes) and\n * re-render only when that slice changes. Use this for non-entity values —\n * entity state goes through `useEntity`. The selector runs on every hass update\n * but only re-renders the consumer when `isEqual` reports a change, so it's\n * cheap for rarely-changing values like config/themes.\n */\nexport function useHassValue<T>(\n selector: (hass: HomeAssistant | undefined) => T,\n isEqual: (a: T, b: T) => boolean = Object.is,\n): T {\n const store = useHAStore();\n\n const selectorRef = useRef(selector);\n selectorRef.current = selector;\n const isEqualRef = useRef(isEqual);\n isEqualRef.current = isEqual;\n\n const [value, setValue] = useState<T>(() => selectorRef.current(store.getHass()));\n\n useEffect(() => {\n const unsubscribe = store.subscribeToHass(() => {\n const next = selectorRef.current(store.getHass());\n setValue((prev) => (isEqualRef.current(prev, next) ? prev : next));\n });\n return unsubscribe;\n }, [store.subscribeToHass, store.getHass]);\n\n return value;\n}\n\n/** Re-renders when `hass.config` changes (units, latitude/longitude, etc.). */\nexport function useHassConfig(): HomeAssistant['config'] | undefined {\n return useHassValue((hass) => hass?.config);\n}\n\n/** Re-renders when the active theme's dark mode flips. */\nexport function useDarkMode(): boolean {\n return useHassValue((hass) => hass?.themes?.darkMode ?? false);\n}\n\ntype ServiceCaller<T extends string> = <S extends keyof ServicesForId<T> & string>(\n service: S,\n ...args: ServicesForId<T>[S] extends undefined\n ? []\n : Record<string, never> extends Exclude<ServicesForId<T>[S], undefined>\n ? [data?: ServicesForId<T>[S]]\n : [data: ServicesForId<T>[S]]\n) => Promise<void>;\n\n/**\n * Returns a stable function that calls services on a specific HA entity.\n * The service domain is parsed from the entity ID prefix and `entity_id` is\n * auto-injected into every call. Service names and data shapes are strongly\n * typed via DomainServiceMap when the domain is registered. No-ops if hass\n * is not yet available or the entity ID is empty.\n *\n * const fanService = useService(config.entity); // `fan.${string}`\n * await fanService('turn_off');\n * await fanService('set_percentage', { percentage: 67 });\n */\nexport function useService<T extends string>(entityId: T): ServiceCaller<T> {\n const { getHass } = useHass();\n return useCallbackStable(((service: string, data?: object) => {\n const hass = getHass();\n if (!hass || !entityId.includes('.')) return Promise.resolve();\n const domain = entityId.split('.', 1)[0];\n return hass.callService(domain, service, { entity_id: entityId, ...data });\n }) as ServiceCaller<T>);\n}\n\ninterface UseCachedFetchResult<T> {\n data: T | undefined;\n status: FetchStatus;\n error: Error | undefined;\n refetch: () => void;\n}\n\n/**\n * Generic hook for fetching data with localStorage caching. Returns a cache-aware\n * status string to distinguish cached vs fresh data.\n */\nexport function useCachedFetch<T>(\n cacheKey: string,\n fetcher: () => Promise<T>,\n deps: unknown[],\n): UseCachedFetchResult<T> {\n const store = useHAStore();\n const [data, setData] = useState<T | undefined>(() => readCache<T>(store.cache, cacheKey));\n const [isFresh, setIsFresh] = useState(false);\n const [isFetching, setIsFetching] = useState(false);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n // Stale-while-revalidate, key-change aware. When `cacheKey` changes we swap to\n // the new key's cached value synchronously (SWR hit) or keep the previously\n // rendered data (keep-previous-data on a cold key) — never blanking to a\n // loading state. The `deps` effect below issues the background refetch; the\n // only `'loading'` state is a true cold start (nothing cached, nothing fetched).\n const dataKeyRef = useRef(cacheKey);\n if (cacheKey !== dataKeyRef.current) {\n dataKeyRef.current = cacheKey;\n setIsFresh(false);\n setError(undefined);\n const cached = readCache<T>(store.cache, cacheKey);\n if (cached !== undefined) setData(cached);\n // cache miss: leave `data` as-is (keep-previous-data)\n }\n\n const fetchIdRef = useRef(0);\n\n const doFetch = useCallbackStable(async () => {\n const fetchId = ++fetchIdRef.current;\n setIsFetching(true);\n setError(undefined);\n\n try {\n const result = await fetcher();\n if (fetchId === fetchIdRef.current) {\n setData(result);\n setIsFresh(true);\n writeCache(store.cache, cacheKey, result);\n }\n } catch (err) {\n if (fetchId === fetchIdRef.current) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n } finally {\n if (fetchId === fetchIdRef.current) {\n setIsFetching(false);\n }\n }\n });\n\n useEffect(() => {\n doFetch();\n }, deps);\n\n const status: FetchStatus = useMemo(() => {\n if (!data && isFetching) return 'loading';\n if (data && !isFresh && isFetching) return 'cached';\n if (data && isFresh && isFetching) return 'refreshing';\n return 'ready';\n }, [data, isFresh, isFetching]);\n\n return { data, status, error, refetch: doFetch };\n}\n\ninterface UseCalendarEventsResult {\n events: CalendarEventWithSource[] | undefined;\n status: FetchStatus;\n error: Error | undefined;\n refetch: () => void;\n /**\n * Warm the cache for an arbitrary range (e.g. adjacent months) without\n * touching component state. Best-effort: skips ranges already cached and\n * swallows failures.\n */\n prefetch: (range: { start: Date; end: Date }) => void;\n}\n\nfunction calendarEventsCacheKey(\n entityIds: `calendar.${string}`[],\n range: { start: Date; end: Date },\n): string {\n return `events:${entityIds.join(',')}:${range.start.getTime()}-${range.end.getTime()}`;\n}\n\nasync function fetchCalendarRange(\n hass: HomeAssistant | undefined,\n entityIds: `calendar.${string}`[],\n range: { start: Date; end: Date },\n): Promise<CalendarEventWithSource[]> {\n if (!hass?.connection) {\n throw new Error('Home Assistant connection not available');\n }\n if (entityIds.length === 0) {\n return [];\n }\n\n const results = await Promise.all(\n entityIds.map(async (entityId) => {\n try {\n const result = await hass.connection.sendMessagePromise<{\n response: { [key: string]: { events: CalendarEvent[] } };\n }>({\n type: 'call_service',\n domain: 'calendar',\n service: 'get_events',\n service_data: {\n start_date_time: range.start.toISOString(),\n end_date_time: range.end.toISOString(),\n },\n target: { entity_id: entityId },\n return_response: true,\n });\n\n const calendarEvents = result.response?.[entityId]?.events ?? [];\n return calendarEvents.map(\n (event): CalendarEventWithSource => ({ ...event, calendarId: entityId }),\n );\n } catch (err) {\n console.error(`Failed to fetch events for ${entityId}:`, err);\n return [];\n }\n }),\n );\n\n return results.flat();\n}\n\n/**\n * Fetch events from one or more calendars for a date range, with in-memory\n * (per-card) caching and stale-while-revalidate behavior. Events are tagged\n * with their source calendar ID. Returns `prefetch` to warm adjacent ranges.\n */\nexport function useCalendarEvents(\n entityIds: `calendar.${string}`[],\n options: { start: Date; end: Date },\n): UseCalendarEventsResult {\n const store = useHAStore();\n const { getHass } = useHass();\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const entityIdsKey = entityIds.join(',');\n const dateRangeKey = `${options.start.getTime()}-${options.end.getTime()}`;\n const cacheKey = `events:${entityIdsKey}:${dateRangeKey}`;\n\n const fetcher = useCallbackStable(() => fetchCalendarRange(getHass(), entityIds, options));\n\n const {\n data: events,\n status,\n error,\n refetch,\n } = useCachedFetch(cacheKey, fetcher, [entityIdsKey, dateRangeKey]);\n\n const prefetch = useCallbackStable((range: { start: Date; end: Date }) => {\n const key = calendarEventsCacheKey(entityIds, range);\n if (store.cache.has(key)) return; // already warm\n fetchCalendarRange(getHass(), entityIds, range)\n .then((result) => writeCache(store.cache, key, result))\n .catch(() => {\n // best-effort prefetch; ignore failures\n });\n });\n\n const debouncedRefetch = useCallbackStable(() => {\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n debounceTimerRef.current = setTimeout(() => refetch(), 500);\n });\n\n useEffect(() => {\n const unsubscribes = entityIds.map((entityId) =>\n store.subscribeToEntity(entityId, debouncedRefetch),\n );\n return () => {\n unsubscribes.forEach((unsub) => unsub());\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n };\n }, [entityIdsKey, store.subscribeToEntity, debouncedRefetch]);\n\n return { events, status, error, refetch, prefetch };\n}\n\ninterface UseWeatherForecastResult {\n forecast: WeatherForecast[] | undefined;\n status: FetchStatus;\n error: Error | undefined;\n refetch: () => void;\n}\n\n/**\n * Fetch weather forecast data with localStorage caching. Auto-refetches at the\n * top of each hour and when the underlying entity changes (debounced).\n */\nexport function useWeatherForecast(\n entityId: `weather.${string}`,\n type: ForecastType,\n): UseWeatherForecastResult {\n const store = useHAStore();\n const { getHass } = useHass();\n const cacheKey = `forecast:${entityId}:${type}`;\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const hourlyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const fetcher = useCallbackStable(async () => {\n const hass = getHass();\n if (!hass?.connection) {\n throw new Error('Home Assistant connection not available');\n }\n\n const result = await hass.connection.sendMessagePromise<{\n response: { [entityId: string]: { forecast: WeatherForecast[] } };\n }>({\n type: 'call_service',\n domain: 'weather',\n service: 'get_forecasts',\n service_data: { type },\n target: { entity_id: entityId },\n return_response: true,\n });\n\n return result.response?.[entityId]?.forecast ?? [];\n });\n\n const {\n data: forecast,\n status,\n error,\n refetch,\n } = useCachedFetch(cacheKey, fetcher, [entityId, type]);\n\n const debouncedRefetch = useCallbackStable(() => {\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n debounceTimerRef.current = setTimeout(() => refetch(), 500);\n });\n\n const scheduleHourlyRefetch = useCallbackStable(() => {\n if (hourlyTimerRef.current) {\n clearTimeout(hourlyTimerRef.current);\n }\n const now = new Date();\n const nextHour = new Date(now);\n nextHour.setHours(now.getHours() + 1, 0, 0, 0);\n const msUntilNextHour = nextHour.getTime() - now.getTime();\n\n hourlyTimerRef.current = setTimeout(() => {\n refetch();\n scheduleHourlyRefetch();\n }, msUntilNextHour);\n });\n\n useEffect(() => {\n scheduleHourlyRefetch();\n }, [entityId, type, scheduleHourlyRefetch]);\n\n useEffect(() => {\n const unsubscribe = store.subscribeToEntity(entityId, debouncedRefetch);\n return () => {\n unsubscribe();\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n if (hourlyTimerRef.current) {\n clearTimeout(hourlyTimerRef.current);\n }\n };\n }, [entityId, store.subscribeToEntity, debouncedRefetch]);\n\n return { forecast, status, error, refetch };\n}\n","// Style registry for Shadow DOM injection\n// Each .styles.ts file uses css`` which auto-registers\n\nconst styleRegistry: string[] = [];\n\n/**\n * CSS tagged template literal for syntax highlighting.\n * Automatically registers the styles with the global registry.\n */\nexport const css = (strings: TemplateStringsArray, ...values: unknown[]): string => {\n const result = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');\n styleRegistry.push(result);\n return result;\n};\n\n/**\n * Register raw CSS string (e.g., from ?inline imports).\n * Only registers if not already present.\n */\nexport function registerRawStyles(styles: string): void {\n if (!styleRegistry.includes(styles)) {\n styleRegistry.push(styles);\n }\n}\n\n/**\n * Get all registered styles for Shadow DOM injection.\n */\nexport function getAllStyles(): string {\n return styleRegistry.join('\\n');\n}\n","import { type ComponentType, render } from 'preact';\nimport { HAProvider } from './HAContext';\nimport { getAllStyles } from './styleRegistry';\nimport type { HomeAssistant } from './types';\n\ninterface RegisterPreactCardOptions<TConfig> {\n type: string;\n name: string;\n description: string;\n Component: ComponentType<{ config: TConfig }>;\n ConfigComponent?: ComponentType<{\n hass: HomeAssistant;\n config: TConfig;\n onConfigChanged: (config: TConfig) => void;\n }>;\n UnconfiguredComponent?: ComponentType<{}>;\n getStubConfig?: () => Partial<TConfig>;\n}\n\ndeclare global {\n interface Window {\n customCards?: Array<{ type: string; name: string; description: string }>;\n }\n}\n\n// Grace period before a disconnected card is torn down. HA detaches and\n// re-attaches cards transiently (scroll virtualization, edit mode); only a card\n// that stays gone past this window is genuinely removed, so we defer the\n// Preact unmount — and the effect cleanups it triggers (timers, subscriptions)\n// — until then. A reconnect within the window cancels the teardown.\nconst TEARDOWN_GRACE_MS = 5000;\n\nexport function registerPreactCard<TConfig>(options: RegisterPreactCardOptions<TConfig>) {\n const {\n type,\n name,\n description,\n Component,\n ConfigComponent,\n UnconfiguredComponent,\n getStubConfig,\n } = options;\n\n // Shared host-element plumbing: config/hass storage, connect/disconnect\n // lifecycle, deferred teardown, and the render skeleton. Subclasses supply\n // the render root and the tree to render, and may override hass behavior.\n abstract class BaseHACard extends HTMLElement {\n protected _hass?: HomeAssistant;\n protected _config?: TConfig;\n private _teardownTimer?: ReturnType<typeof setTimeout>;\n\n protected abstract _getRenderRoot(): Element | ShadowRoot;\n protected abstract _renderTree(): void;\n protected _renderUnconfigured(): void {}\n\n connectedCallback() {\n if (this._teardownTimer !== undefined) {\n clearTimeout(this._teardownTimer);\n this._teardownTimer = undefined;\n }\n this._maybeRenderOnConnect();\n }\n\n protected _maybeRenderOnConnect() {\n if (this._hass && this._config) {\n this._render();\n }\n }\n\n disconnectedCallback() {\n if (this._teardownTimer !== undefined) clearTimeout(this._teardownTimer);\n this._teardownTimer = setTimeout(() => {\n // Unmount the Preact tree, running effect cleanups (clears timers and\n // entity subscriptions). Scheduled, never synchronous, so a transient\n // disconnect+reconnect leaves the tree intact.\n render(null, this._getRenderRoot());\n this._teardownTimer = undefined;\n }, TEARDOWN_GRACE_MS);\n }\n\n setConfig(config: TConfig) {\n this._config = config;\n if (this._hass && this.isConnected) {\n this._render();\n }\n }\n\n protected _render() {\n if (!this._config || !this._hass) {\n this._renderUnconfigured();\n return;\n }\n this._renderTree();\n }\n }\n\n class HACard extends BaseHACard {\n private _shadowRoot: ShadowRoot;\n private _entityChangeListeners = new Map<string, Set<(entity: any) => void>>();\n private _hassChangeListeners = new Set<() => void>();\n\n constructor() {\n super();\n this._shadowRoot = this.attachShadow({ mode: 'open' });\n }\n\n protected _getRenderRoot() {\n return this._shadowRoot;\n }\n\n set hass(hass: HomeAssistant) {\n const prevStates = this._hass?.states;\n this._hass = hass;\n\n for (const [entityId, listeners] of this._entityChangeListeners) {\n const newState = hass.states[entityId];\n const oldState = prevStates?.[entityId];\n if (newState !== oldState) {\n listeners.forEach((listener) => listener(newState));\n }\n }\n\n // Notify non-entity (config/themes) subscribers on every update. This does\n // not re-render the whole card — useHassValue re-renders only its own\n // consumer, and only when its selected slice actually changes.\n this._hassChangeListeners.forEach((listener) => listener());\n\n if (!prevStates && this._config && this.isConnected) {\n this._render();\n }\n }\n\n private _subscribeToEntity = (entityId: string, callback: (entity: any) => void) => {\n if (!this._entityChangeListeners.has(entityId)) {\n this._entityChangeListeners.set(entityId, new Set());\n }\n this._entityChangeListeners.get(entityId)!.add(callback);\n\n return () => {\n const listeners = this._entityChangeListeners.get(entityId);\n if (listeners) {\n listeners.delete(callback);\n if (listeners.size === 0) {\n this._entityChangeListeners.delete(entityId);\n }\n }\n };\n };\n\n private _subscribeToHass = (callback: () => void) => {\n this._hassChangeListeners.add(callback);\n return () => {\n this._hassChangeListeners.delete(callback);\n };\n };\n\n protected _renderTree() {\n render(\n <HAProvider\n hass={this._hass}\n subscribeToEntity={this._subscribeToEntity}\n subscribeToHass={this._subscribeToHass}\n >\n <style>{getAllStyles()}</style>\n <Component config={this._config!} />\n </HAProvider>,\n this._shadowRoot,\n );\n }\n\n protected _renderUnconfigured() {\n if (UnconfiguredComponent) {\n render(<UnconfiguredComponent />, this._shadowRoot);\n }\n }\n\n static getConfigElement() {\n if (ConfigComponent) {\n return document.createElement(`${type}-editor`);\n }\n return undefined;\n }\n\n static getStubConfig() {\n return getStubConfig?.() ?? {};\n }\n }\n\n customElements.define(type, HACard);\n\n if (ConfigComponent) {\n const EditorComponent = ConfigComponent;\n\n class HACardEditor extends BaseHACard {\n protected _getRenderRoot() {\n return this;\n }\n\n // The editor re-renders on every hass update: it passes `hass` straight to\n // HA's <ha-form>/<ha-selector>, whose entity pickers need a fresh hass to\n // stay current. (Unlike the card, which renders once then subscribes.)\n set hass(hass: HomeAssistant) {\n this._hass = hass;\n this._render();\n }\n\n // Render whenever config arrives, regardless of connection — HA may set\n // config/hass before connecting the editor element.\n setConfig(config: TConfig) {\n this._config = config;\n this._render();\n }\n\n private _fireConfigChanged = (config: TConfig) => {\n this.dispatchEvent(\n new CustomEvent('config-changed', {\n detail: { config },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n // Render to light DOM so HA's custom elements (ha-form etc.) work.\n protected _renderTree() {\n render(\n <EditorComponent\n hass={this._hass!}\n config={this._config!}\n onConfigChanged={this._fireConfigChanged}\n />,\n this,\n );\n }\n }\n\n customElements.define(`${type}-editor`, HACardEditor);\n }\n\n window.customCards = window.customCards || [];\n window.customCards.push({ type, name, description });\n\n console.info(\n `%c ${name.toUpperCase()} %c loaded `,\n 'background: #3b82f6; color: white; font-weight: bold',\n '',\n );\n}\n","import type { RefObject } from 'preact';\nimport { useEffect, useRef } from 'preact/hooks';\n\nexport interface ElementSize {\n width: number;\n height: number;\n}\n\nexport type ResizeCallback = (size: ElementSize) => void;\n\n/**\n * Observe an element's size via ResizeObserver. The callback fires:\n *\n * 1. Once after mount, with the element's current size.\n * 2. Whenever the element's size changes.\n * 3. Whenever `deps` change, re-firing with the current size — so callers\n * can re-run draws when their inputs change without re-creating the\n * observer.\n *\n * The callback is suppressed only while the element is detached from the\n * document. Zero width/height is delivered to the callback as-is — consumers\n * that need to skip degenerate sizes (e.g. canvas painters where a 0-sized\n * drawImage throws InvalidStateError) should add their own early return.\n *\n * Sizes are read from `offsetWidth` / `offsetHeight` (CSS pixels, includes\n * padding + border). The callback is held in a ref, so passing a fresh\n * function each render is safe — it never re-creates the observer.\n *\n * @example\n * const containerRef = useRef<HTMLDivElement>(null);\n * const canvasRef = useRef<HTMLCanvasElement>(null);\n *\n * useResizeObserver(\n * containerRef,\n * ({ width, height }) => {\n * if (width === 0 || height === 0) return; // optional, consumer's call\n * drawChart(canvasRef.current, forecast, width, height);\n * },\n * [forecast],\n * );\n */\nexport function useResizeObserver<T extends HTMLElement>(\n ref: RefObject<T>,\n callback: ResizeCallback,\n deps: unknown[] = [],\n): void {\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n // Set up the observer once per element. ResizeObserver fires once\n // synchronously-ish after `.observe()` with the current size, which\n // covers the initial draw.\n useEffect(() => {\n const element = ref.current;\n if (!element) return;\n\n const fire = () => {\n if (!element.isConnected) return;\n callbackRef.current({\n width: element.offsetWidth,\n height: element.offsetHeight,\n });\n };\n\n const observer = new ResizeObserver(fire);\n observer.observe(element);\n return () => observer.disconnect();\n // ref identity is stable across renders; observer setup runs once.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Re-fire on dependency change. Skip the first render — the observer's\n // initial `.observe()` fire already delivers the mount-time size.\n const isFirstRun = useRef(true);\n useEffect(() => {\n if (isFirstRun.current) {\n isFirstRun.current = false;\n return;\n }\n const element = ref.current;\n if (!element || !element.isConnected) return;\n callbackRef.current({\n width: element.offsetWidth,\n height: element.offsetHeight,\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n}\n","import type { RefObject } from 'preact';\nimport { useState } from 'preact/hooks';\nimport { useResizeObserver } from './useResizeObserver';\n\n/**\n * Track the current width of a referenced element in CSS pixels.\n *\n * Returns `undefined` until the first non-zero measurement is observed, then\n * a positive number that updates as the element resizes. Once a real width\n * is captured the hook will never report `undefined` or `0` again, even\n * during HA layout transitions (dashboard switch, edit-mode toggle) — the\n * underlying ResizeObserver firings are silently dropped while the element\n * is detached or transiently zero-width, so the component renders with the\n * last good value instead of flashing through a degenerate state.\n *\n * Use this when you need a width value in JSX (responsive layout, prop to a\n * sized child). If you only need the value imperatively inside a draw\n * callback, prefer `useResizeObserver` directly — it doesn't allocate\n * component state or cause re-renders.\n *\n * @example\n * const ref = useRef<HTMLDivElement>(null);\n * const width = useWidth(ref);\n * return (\n * <div ref={ref}>\n * {width !== undefined && <Chart width={width} />}\n * </div>\n * );\n */\nexport function useWidth<T extends HTMLElement>(ref: RefObject<T>): number | undefined {\n const [width, setWidth] = useState<number | undefined>(undefined);\n useResizeObserver(ref, (size) => {\n if (size.width === 0) return;\n setWidth((prev) => (prev === size.width ? prev : size.width));\n });\n return width;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/cacheUtils.ts","../src/useCallbackStable.ts","../src/HAContext.tsx","../src/styleRegistry.ts","../src/registerPreactCard.tsx","../src/HACard.tsx","../src/calendars.ts","../src/useResizeObserver.ts","../src/useWidth.ts"],"sourcesContent":["// In-memory cache helpers. The cache Map is owned per-card by the HAProvider\n// store (see HAContext), so its lifetime matches the card and it is\n// garbage-collected when the card is torn down. There is intentionally no\n// persistence, TTL, or size cap: freshness comes from entity subscriptions and\n// periodic refetch, not from cache expiry, and growth is bounded by the ranges\n// a single card visits in a session.\n\nexport interface CacheEntry<T> {\n data: T;\n}\n\nexport type Cache = Map<string, CacheEntry<unknown>>;\n\nexport function readCache<T>(cache: Cache, key: string): T | undefined {\n return (cache.get(key) as CacheEntry<T> | undefined)?.data;\n}\n\nexport function writeCache<T>(cache: Cache, key: string, data: T): void {\n cache.set(key, { data });\n}\n","import { useRef } from 'preact/hooks';\n\n/**\n * Creates a stable callback reference that always calls the latest version of the callback.\n * Unlike useCallback, this never changes identity, so it won't cause re-renders in children.\n *\n * @param callback The callback function to stabilize\n * @returns A stable function reference that always calls the latest callback\n */\nexport function useCallbackStable<T extends (...args: never[]) => unknown>(callback: T): T {\n const callbackRef = useRef<T>(callback);\n\n callbackRef.current = callback;\n\n // Create a stable function reference once\n const stableRef = useRef<T | null>(null);\n if (stableRef.current === null) {\n stableRef.current = ((...args: Parameters<T>) => {\n return callbackRef.current(...args);\n }) as T;\n }\n\n return stableRef.current;\n}\n","import { createContext } from 'preact';\nimport type { ComponentChildren } from 'preact';\nimport { useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';\n\nimport { type Cache, readCache, writeCache } from './cacheUtils';\nimport type {\n EntityForId,\n FetchStatus,\n ForecastType,\n HomeAssistant,\n ServicesForId,\n WeatherForecast,\n} from './types';\nimport { useCallbackStable } from './useCallbackStable';\n\ntype SubscribeToHass = (callback: () => void) => () => void;\n\n// Default for providers that don't wire up hass-value notifications (Storybook,\n// tests). useHassValue then simply returns its initial value and never updates.\nconst noopSubscribeToHass: SubscribeToHass = () => () => {};\n\ninterface HAStore {\n getHass: () => HomeAssistant | undefined;\n subscribeToEntity: (entityId: string, callback: (entity: any) => void) => () => void;\n subscribeToHass: SubscribeToHass;\n // Per-card cache (events, forecasts, entities). Owned by the provider so its\n // lifetime is the card's — GC'd with the store when the card is torn down.\n cache: Cache;\n}\n\nconst HAContext = createContext<HAStore | null>(null);\n\ninterface HAProviderProps {\n hass: HomeAssistant | undefined;\n subscribeToEntity: (entityId: string, callback: (entity: any) => void) => () => void;\n subscribeToHass?: SubscribeToHass;\n // Optional injected cache (tests seed/inspect it); defaults to a fresh\n // per-provider Map held stable across re-renders.\n cache?: Cache;\n children: ComponentChildren;\n}\n\nexport function HAProvider({\n hass,\n subscribeToEntity,\n subscribeToHass,\n cache,\n children,\n}: HAProviderProps) {\n const hassRef = useRef(hass);\n hassRef.current = hass;\n\n const getHass = useCallbackStable(() => hassRef.current);\n\n const cacheRef = useRef<Cache>();\n if (!cacheRef.current) cacheRef.current = cache ?? new Map();\n\n const resolvedSubscribeToHass = subscribeToHass ?? noopSubscribeToHass;\n\n const store = useMemo<HAStore>(\n () => ({\n getHass,\n subscribeToEntity,\n subscribeToHass: resolvedSubscribeToHass,\n cache: cacheRef.current!,\n }),\n [getHass, subscribeToEntity, resolvedSubscribeToHass],\n );\n\n return <HAContext.Provider value={store}>{children}</HAContext.Provider>;\n}\n\n/** Internal accessor for the provider store (cache, subscriptions). Not part of the public API. */\nexport function useHAStore(): HAStore {\n const store = useContext(HAContext);\n if (!store) {\n throw new Error('useEntity/useHass must be used within an HAProvider');\n }\n return store;\n}\n\n/**\n * Subscribe to a specific entity by ID. Re-renders only when that entity changes.\n *\n * Returns a typed entity based on the domain prefix:\n * - 'calendar.xyz' -> CalendarEntity\n * - 'weather.xyz' -> WeatherEntity\n * - 'sun.sun' -> SunEntity\n * - other domains -> HassEntity (fallback)\n */\nexport function useEntity<T extends string>(entityId: T): EntityForId<T> | undefined {\n const store = useHAStore();\n const cacheKey = `entity:${entityId}`;\n\n const [entity, setEntity] = useState<EntityForId<T> | undefined>(() => {\n const current = store.getHass()?.states[entityId] as EntityForId<T> | undefined;\n if (current) return current;\n return readCache<EntityForId<T>>(store.cache, cacheKey);\n });\n\n useEffect(() => {\n const unsubscribe = store.subscribeToEntity(entityId, (newEntity) => {\n setEntity(newEntity as EntityForId<T>);\n writeCache(store.cache, cacheKey, newEntity);\n });\n return unsubscribe;\n }, [entityId, store.subscribeToEntity, store.cache, cacheKey]);\n\n return entity;\n}\n\n/**\n * Get access to the full hass object for calling services / accessing config.\n * Does NOT re-render on entity changes. Use useEntity for that.\n */\nexport function useHass(): { getHass: () => HomeAssistant | undefined } {\n const store = useHAStore();\n return { getHass: store.getHass };\n}\n\n/**\n * Subscribe to a derived slice of the `hass` object (e.g. config, themes) and\n * re-render only when that slice changes. Use this for non-entity values —\n * entity state goes through `useEntity`. The selector runs on every hass update\n * but only re-renders the consumer when `isEqual` reports a change, so it's\n * cheap for rarely-changing values like config/themes.\n */\nexport function useHassValue<T>(\n selector: (hass: HomeAssistant | undefined) => T,\n isEqual: (a: T, b: T) => boolean = Object.is,\n): T {\n const store = useHAStore();\n\n const selectorRef = useRef(selector);\n selectorRef.current = selector;\n const isEqualRef = useRef(isEqual);\n isEqualRef.current = isEqual;\n\n const [value, setValue] = useState<T>(() => selectorRef.current(store.getHass()));\n\n useEffect(() => {\n const unsubscribe = store.subscribeToHass(() => {\n const next = selectorRef.current(store.getHass());\n setValue((prev) => (isEqualRef.current(prev, next) ? prev : next));\n });\n return unsubscribe;\n }, [store.subscribeToHass, store.getHass]);\n\n return value;\n}\n\n/** Re-renders when `hass.config` changes (units, latitude/longitude, etc.). */\nexport function useHassConfig(): HomeAssistant['config'] | undefined {\n return useHassValue((hass) => hass?.config);\n}\n\n/** Re-renders when the active theme's dark mode flips. */\nexport function useDarkMode(): boolean {\n return useHassValue((hass) => hass?.themes?.darkMode ?? false);\n}\n\ntype ServiceCaller<T extends string> = <S extends keyof ServicesForId<T> & string>(\n service: S,\n ...args: ServicesForId<T>[S] extends undefined\n ? []\n : Record<string, never> extends Exclude<ServicesForId<T>[S], undefined>\n ? [data?: ServicesForId<T>[S]]\n : [data: ServicesForId<T>[S]]\n) => Promise<void>;\n\n/**\n * Returns a stable function that calls services on a specific HA entity.\n * The service domain is parsed from the entity ID prefix and `entity_id` is\n * auto-injected into every call. Service names and data shapes are strongly\n * typed via DomainServiceMap when the domain is registered. No-ops if hass\n * is not yet available or the entity ID is empty.\n *\n * const fanService = useService(config.entity); // `fan.${string}`\n * await fanService('turn_off');\n * await fanService('set_percentage', { percentage: 67 });\n */\nexport function useService<T extends string>(entityId: T): ServiceCaller<T> {\n const { getHass } = useHass();\n return useCallbackStable(((service: string, data?: object) => {\n const hass = getHass();\n if (!hass || !entityId.includes('.')) return Promise.resolve();\n const domain = entityId.split('.', 1)[0];\n return hass.callService(domain, service, { entity_id: entityId, ...data });\n }) as ServiceCaller<T>);\n}\n\ninterface UseCachedFetchResult<T> {\n data: T | undefined;\n status: FetchStatus;\n error: Error | undefined;\n refetch: () => void;\n}\n\n/**\n * Generic hook for fetching data with localStorage caching. Returns a cache-aware\n * status string to distinguish cached vs fresh data.\n */\nexport function useCachedFetch<T>(\n cacheKey: string,\n fetcher: () => Promise<T>,\n deps: unknown[],\n): UseCachedFetchResult<T> {\n const store = useHAStore();\n const [data, setData] = useState<T | undefined>(() => readCache<T>(store.cache, cacheKey));\n const [isFresh, setIsFresh] = useState(false);\n const [isFetching, setIsFetching] = useState(false);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n // Stale-while-revalidate, key-change aware. When `cacheKey` changes we swap to\n // the new key's cached value synchronously (SWR hit) or keep the previously\n // rendered data (keep-previous-data on a cold key) — never blanking to a\n // loading state. The `deps` effect below issues the background refetch; the\n // only `'loading'` state is a true cold start (nothing cached, nothing fetched).\n const dataKeyRef = useRef(cacheKey);\n if (cacheKey !== dataKeyRef.current) {\n dataKeyRef.current = cacheKey;\n setIsFresh(false);\n setError(undefined);\n const cached = readCache<T>(store.cache, cacheKey);\n if (cached !== undefined) setData(cached);\n // cache miss: leave `data` as-is (keep-previous-data)\n }\n\n const fetchIdRef = useRef(0);\n\n const doFetch = useCallbackStable(async () => {\n const fetchId = ++fetchIdRef.current;\n setIsFetching(true);\n setError(undefined);\n\n try {\n const result = await fetcher();\n if (fetchId === fetchIdRef.current) {\n setData(result);\n setIsFresh(true);\n writeCache(store.cache, cacheKey, result);\n }\n } catch (err) {\n if (fetchId === fetchIdRef.current) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n } finally {\n if (fetchId === fetchIdRef.current) {\n setIsFetching(false);\n }\n }\n });\n\n useEffect(() => {\n doFetch();\n }, deps);\n\n const status: FetchStatus = useMemo(() => {\n if (!data && isFetching) return 'loading';\n if (data && !isFresh && isFetching) return 'cached';\n if (data && isFresh && isFetching) return 'refreshing';\n return 'ready';\n }, [data, isFresh, isFetching]);\n\n return { data, status, error, refetch: doFetch };\n}\n\ninterface UseWeatherForecastResult {\n forecast: WeatherForecast[] | undefined;\n status: FetchStatus;\n error: Error | undefined;\n refetch: () => void;\n}\n\n/**\n * Fetch weather forecast data with localStorage caching. Auto-refetches at the\n * top of each hour and when the underlying entity changes (debounced).\n */\nexport function useWeatherForecast(\n entityId: `weather.${string}`,\n type: ForecastType,\n): UseWeatherForecastResult {\n const store = useHAStore();\n const { getHass } = useHass();\n const cacheKey = `forecast:${entityId}:${type}`;\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const hourlyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const fetcher = useCallbackStable(async () => {\n const hass = getHass();\n if (!hass?.connection) {\n throw new Error('Home Assistant connection not available');\n }\n\n const result = await hass.connection.sendMessagePromise<{\n response: { [entityId: string]: { forecast: WeatherForecast[] } };\n }>({\n type: 'call_service',\n domain: 'weather',\n service: 'get_forecasts',\n service_data: { type },\n target: { entity_id: entityId },\n return_response: true,\n });\n\n return result.response?.[entityId]?.forecast ?? [];\n });\n\n const {\n data: forecast,\n status,\n error,\n refetch,\n } = useCachedFetch(cacheKey, fetcher, [entityId, type]);\n\n const debouncedRefetch = useCallbackStable(() => {\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n debounceTimerRef.current = setTimeout(() => refetch(), 500);\n });\n\n const scheduleHourlyRefetch = useCallbackStable(() => {\n if (hourlyTimerRef.current) {\n clearTimeout(hourlyTimerRef.current);\n }\n const now = new Date();\n const nextHour = new Date(now);\n nextHour.setHours(now.getHours() + 1, 0, 0, 0);\n const msUntilNextHour = nextHour.getTime() - now.getTime();\n\n hourlyTimerRef.current = setTimeout(() => {\n refetch();\n scheduleHourlyRefetch();\n }, msUntilNextHour);\n });\n\n useEffect(() => {\n scheduleHourlyRefetch();\n }, [entityId, type, scheduleHourlyRefetch]);\n\n useEffect(() => {\n const unsubscribe = store.subscribeToEntity(entityId, debouncedRefetch);\n return () => {\n unsubscribe();\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n if (hourlyTimerRef.current) {\n clearTimeout(hourlyTimerRef.current);\n }\n };\n }, [entityId, store.subscribeToEntity, debouncedRefetch]);\n\n return { forecast, status, error, refetch };\n}\n","// Style registry for Shadow DOM injection\n// Each .styles.ts file uses css`` which auto-registers\n\nconst styleRegistry: string[] = [];\n\n/**\n * CSS tagged template literal for syntax highlighting.\n * Automatically registers the styles with the global registry.\n */\nexport const css = (strings: TemplateStringsArray, ...values: unknown[]): string => {\n const result = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');\n styleRegistry.push(result);\n return result;\n};\n\n/**\n * Register raw CSS string (e.g., from ?inline imports).\n * Only registers if not already present.\n */\nexport function registerRawStyles(styles: string): void {\n if (!styleRegistry.includes(styles)) {\n styleRegistry.push(styles);\n }\n}\n\n/**\n * Get all registered styles for Shadow DOM injection.\n */\nexport function getAllStyles(): string {\n return styleRegistry.join('\\n');\n}\n","import { type ComponentType, render } from 'preact';\nimport { HAProvider } from './HAContext';\nimport { getAllStyles } from './styleRegistry';\nimport type { HomeAssistant } from './types';\n\ninterface RegisterPreactCardOptions<TConfig> {\n type: string;\n name: string;\n description: string;\n Component: ComponentType<{ config: TConfig }>;\n ConfigComponent?: ComponentType<{\n hass: HomeAssistant;\n config: TConfig;\n onConfigChanged: (config: TConfig) => void;\n }>;\n UnconfiguredComponent?: ComponentType<{}>;\n getStubConfig?: () => Partial<TConfig>;\n}\n\ndeclare global {\n interface Window {\n customCards?: Array<{ type: string; name: string; description: string }>;\n }\n}\n\n// Grace period before a disconnected card is torn down. HA detaches and\n// re-attaches cards transiently (scroll virtualization, edit mode); only a card\n// that stays gone past this window is genuinely removed, so we defer the\n// Preact unmount — and the effect cleanups it triggers (timers, subscriptions)\n// — until then. A reconnect within the window cancels the teardown.\nconst TEARDOWN_GRACE_MS = 5000;\n\nexport function registerPreactCard<TConfig>(options: RegisterPreactCardOptions<TConfig>) {\n const {\n type,\n name,\n description,\n Component,\n ConfigComponent,\n UnconfiguredComponent,\n getStubConfig,\n } = options;\n\n // Shared host-element plumbing: config/hass storage, connect/disconnect\n // lifecycle, deferred teardown, and the render skeleton. Subclasses supply\n // the render root and the tree to render, and may override hass behavior.\n abstract class BaseHACard extends HTMLElement {\n protected _hass?: HomeAssistant;\n protected _config?: TConfig;\n private _teardownTimer?: ReturnType<typeof setTimeout>;\n\n protected abstract _getRenderRoot(): Element | ShadowRoot;\n protected abstract _renderTree(): void;\n protected _renderUnconfigured(): void {}\n\n connectedCallback() {\n if (this._teardownTimer !== undefined) {\n clearTimeout(this._teardownTimer);\n this._teardownTimer = undefined;\n }\n this._maybeRenderOnConnect();\n }\n\n protected _maybeRenderOnConnect() {\n if (this._hass && this._config) {\n this._render();\n }\n }\n\n disconnectedCallback() {\n if (this._teardownTimer !== undefined) clearTimeout(this._teardownTimer);\n this._teardownTimer = setTimeout(() => {\n // Unmount the Preact tree, running effect cleanups (clears timers and\n // entity subscriptions). Scheduled, never synchronous, so a transient\n // disconnect+reconnect leaves the tree intact.\n render(null, this._getRenderRoot());\n this._teardownTimer = undefined;\n }, TEARDOWN_GRACE_MS);\n }\n\n setConfig(config: TConfig) {\n this._config = config;\n if (this._hass && this.isConnected) {\n this._render();\n }\n }\n\n protected _render() {\n if (!this._config || !this._hass) {\n this._renderUnconfigured();\n return;\n }\n this._renderTree();\n }\n }\n\n class HACard extends BaseHACard {\n private _shadowRoot: ShadowRoot;\n private _entityChangeListeners = new Map<string, Set<(entity: any) => void>>();\n private _hassChangeListeners = new Set<() => void>();\n\n constructor() {\n super();\n this._shadowRoot = this.attachShadow({ mode: 'open' });\n }\n\n protected _getRenderRoot() {\n return this._shadowRoot;\n }\n\n set hass(hass: HomeAssistant) {\n const prevStates = this._hass?.states;\n this._hass = hass;\n\n for (const [entityId, listeners] of this._entityChangeListeners) {\n const newState = hass.states[entityId];\n const oldState = prevStates?.[entityId];\n if (newState !== oldState) {\n listeners.forEach((listener) => listener(newState));\n }\n }\n\n // Notify non-entity (config/themes) subscribers on every update. This does\n // not re-render the whole card — useHassValue re-renders only its own\n // consumer, and only when its selected slice actually changes.\n this._hassChangeListeners.forEach((listener) => listener());\n\n if (!prevStates && this._config && this.isConnected) {\n this._render();\n }\n }\n\n private _subscribeToEntity = (entityId: string, callback: (entity: any) => void) => {\n if (!this._entityChangeListeners.has(entityId)) {\n this._entityChangeListeners.set(entityId, new Set());\n }\n this._entityChangeListeners.get(entityId)!.add(callback);\n\n return () => {\n const listeners = this._entityChangeListeners.get(entityId);\n if (listeners) {\n listeners.delete(callback);\n if (listeners.size === 0) {\n this._entityChangeListeners.delete(entityId);\n }\n }\n };\n };\n\n private _subscribeToHass = (callback: () => void) => {\n this._hassChangeListeners.add(callback);\n return () => {\n this._hassChangeListeners.delete(callback);\n };\n };\n\n protected _renderTree() {\n render(\n <HAProvider\n hass={this._hass}\n subscribeToEntity={this._subscribeToEntity}\n subscribeToHass={this._subscribeToHass}\n >\n <style>{getAllStyles()}</style>\n <Component config={this._config!} />\n </HAProvider>,\n this._shadowRoot,\n );\n }\n\n protected _renderUnconfigured() {\n if (UnconfiguredComponent) {\n render(<UnconfiguredComponent />, this._shadowRoot);\n }\n }\n\n static getConfigElement() {\n if (ConfigComponent) {\n return document.createElement(`${type}-editor`);\n }\n return undefined;\n }\n\n static getStubConfig() {\n return getStubConfig?.() ?? {};\n }\n }\n\n customElements.define(type, HACard);\n\n if (ConfigComponent) {\n const EditorComponent = ConfigComponent;\n\n class HACardEditor extends BaseHACard {\n protected _getRenderRoot() {\n return this;\n }\n\n // The editor re-renders on every hass update: it passes `hass` straight to\n // HA's <ha-form>/<ha-selector>, whose entity pickers need a fresh hass to\n // stay current. (Unlike the card, which renders once then subscribes.)\n set hass(hass: HomeAssistant) {\n this._hass = hass;\n this._render();\n }\n\n // Render whenever config arrives, regardless of connection — HA may set\n // config/hass before connecting the editor element.\n setConfig(config: TConfig) {\n this._config = config;\n this._render();\n }\n\n private _fireConfigChanged = (config: TConfig) => {\n this.dispatchEvent(\n new CustomEvent('config-changed', {\n detail: { config },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n // Render to light DOM so HA's custom elements (ha-form etc.) work.\n protected _renderTree() {\n render(\n <EditorComponent\n hass={this._hass!}\n config={this._config!}\n onConfigChanged={this._fireConfigChanged}\n />,\n this,\n );\n }\n }\n\n customElements.define(`${type}-editor`, HACardEditor);\n }\n\n window.customCards = window.customCards || [];\n window.customCards.push({ type, name, description });\n\n console.info(\n `%c ${name.toUpperCase()} %c loaded `,\n 'background: #3b82f6; color: white; font-weight: bold',\n '',\n );\n}\n","import { type ComponentChildren, createElement } from 'preact';\n\n// `align` controls how content sits within the height HA assigns. Friendly\n// aliases map to flex justify-content; any raw flex value also passes through.\n// Default 'top' keeps content compact at the top (today's look, but the card\n// box now fills the slot). Use 'center', 'bottom', or 'space-between' to spread.\nexport type HACardAlign =\n | 'top'\n | 'center'\n | 'bottom'\n | 'flex-start'\n | 'flex-end'\n | 'space-between'\n | 'space-around'\n | 'space-evenly';\n\nconst ALIGN_ALIASES: Record<string, string> = { top: 'flex-start', bottom: 'flex-end' };\n\ninterface HACardProps {\n align?: HACardAlign;\n class?: string;\n children?: ComponentChildren;\n}\n\n// Drop-in replacement for a raw <ha-card> that fills the height Home Assistant\n// assigns. In the sections (grid) layout HA gives the card's host element a\n// definite height; a plain ha-card collapses to natural content height and\n// renders slightly short. HACard makes the host and ha-card fill that height\n// and flex-distributes content per `align`.\n//\n// `ha-card` is created via createElement (rather than as a JSX intrinsic) so\n// the component carries no dependency on consumers declaring it in their JSX\n// IntrinsicElements — it just works when imported.\nexport function HACard({ align = 'top', class: className, children }: HACardProps) {\n const justify = ALIGN_ALIASES[align] ?? align;\n return (\n <>\n {/* Host must have a definite height for ha-card's 100% to resolve. In\n masonry/auto-row layouts the parent height is auto, so this safely\n collapses back to natural height. */}\n <style>{':host{display:block;height:100%;box-sizing:border-box;}'}</style>\n {createElement(\n 'ha-card',\n {\n class: className,\n style: {\n height: '100%',\n boxSizing: 'border-box',\n display: 'flex',\n flexDirection: 'column',\n justifyContent: justify,\n },\n },\n children,\n )}\n </>\n );\n}\n","import { useEffect, useRef } from 'preact/hooks';\n\nimport { useCachedFetch, useHAStore, useHass } from './HAContext';\nimport { writeCache } from './cacheUtils';\nimport type { CalendarEvent, CalendarEventWithSource, FetchStatus, HomeAssistant } from './types';\nimport { useCallbackStable } from './useCallbackStable';\n\ninterface UseCalendarEventsResult {\n events: CalendarEventWithSource[] | undefined;\n status: FetchStatus;\n error: Error | undefined;\n refetch: () => void;\n /**\n * Warm the cache for an arbitrary range (e.g. adjacent months) without\n * touching component state. Best-effort: skips ranges already cached and\n * swallows failures.\n */\n prefetch: (range: { start: Date; end: Date }) => void;\n}\n\nfunction calendarEventsCacheKey(\n entityIds: `calendar.${string}`[],\n range: { start: Date; end: Date },\n): string {\n return `events:${entityIds.join(',')}:${range.start.getTime()}-${range.end.getTime()}`;\n}\n\n/** Event shape returned by the REST view GET /api/calendars/{entity_id}. */\ninterface ApiCalendarEvent {\n summary: string;\n description?: string | null;\n location?: string | null;\n uid?: string | null;\n recurrence_id?: string | null;\n rrule?: string | null;\n start: { dateTime?: string; date?: string };\n end: { dateTime?: string; date?: string };\n}\n\n/** Flatten the REST API's {dateTime}/{date} start/end into our string form. */\nfunction apiDateString(value: { dateTime?: string; date?: string }): string {\n return value.dateTime ?? value.date ?? '';\n}\n\nasync function fetchEntityEventsRest(\n hass: HomeAssistant,\n entityId: `calendar.${string}`,\n range: { start: Date; end: Date },\n): Promise<CalendarEvent[]> {\n const query =\n `start=${encodeURIComponent(range.start.toISOString())}` +\n `&end=${encodeURIComponent(range.end.toISOString())}`;\n const apiEvents = await hass.callApi!<ApiCalendarEvent[]>(\n 'GET',\n `calendars/${entityId}?${query}`,\n );\n return apiEvents.map((event) => ({\n start: apiDateString(event.start),\n end: apiDateString(event.end),\n summary: event.summary,\n ...(event.description != null && { description: event.description }),\n ...(event.location != null && { location: event.location }),\n ...(event.uid != null && { uid: event.uid }),\n ...(event.recurrence_id != null && { recurrence_id: event.recurrence_id }),\n ...(event.rrule != null && { rrule: event.rrule }),\n }));\n}\n\nasync function fetchEntityEventsWs(\n hass: HomeAssistant,\n entityId: `calendar.${string}`,\n range: { start: Date; end: Date },\n): Promise<CalendarEvent[]> {\n const result = await hass.connection.sendMessagePromise<{\n response: { [key: string]: { events: CalendarEvent[] } };\n }>({\n type: 'call_service',\n domain: 'calendar',\n service: 'get_events',\n service_data: {\n start_date_time: range.start.toISOString(),\n end_date_time: range.end.toISOString(),\n },\n target: { entity_id: entityId },\n return_response: true,\n });\n\n return result.response?.[entityId]?.events ?? [];\n}\n\nasync function fetchCalendarRange(\n hass: HomeAssistant | undefined,\n entityIds: `calendar.${string}`[],\n range: { start: Date; end: Date },\n): Promise<CalendarEventWithSource[]> {\n if (!hass?.connection) {\n throw new Error('Home Assistant connection not available');\n }\n if (entityIds.length === 0) {\n return [];\n }\n\n const results = await Promise.all(\n entityIds.map(async (entityId) => {\n try {\n // Prefer the REST view: unlike the calendar.get_events service\n // response (filtered to LIST_EVENT_FIELDS in HA core), it includes\n // uid/recurrence_id/rrule. The WS service call remains as a fallback\n // for environments without callApi (test mocks, Storybook).\n const calendarEvents = hass.callApi\n ? await fetchEntityEventsRest(hass, entityId, range)\n : await fetchEntityEventsWs(hass, entityId, range);\n return calendarEvents.map(\n (event): CalendarEventWithSource => ({ ...event, calendarId: entityId }),\n );\n } catch (err) {\n console.error(`Failed to fetch events for ${entityId}:`, err);\n return [];\n }\n }),\n );\n\n return results.flat();\n}\n\n/**\n * Fetch events from one or more calendars for a date range, with in-memory\n * (per-card) caching and stale-while-revalidate behavior. Events are tagged\n * with their source calendar ID. Returns `prefetch` to warm adjacent ranges.\n */\nexport function useCalendarEvents(\n entityIds: `calendar.${string}`[],\n options: { start: Date; end: Date },\n): UseCalendarEventsResult {\n const store = useHAStore();\n const { getHass } = useHass();\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const entityIdsKey = entityIds.join(',');\n const dateRangeKey = `${options.start.getTime()}-${options.end.getTime()}`;\n const cacheKey = `events:${entityIdsKey}:${dateRangeKey}`;\n\n const fetcher = useCallbackStable(() => fetchCalendarRange(getHass(), entityIds, options));\n\n const {\n data: events,\n status,\n error,\n refetch,\n } = useCachedFetch(cacheKey, fetcher, [entityIdsKey, dateRangeKey]);\n\n const prefetch = useCallbackStable((range: { start: Date; end: Date }) => {\n const key = calendarEventsCacheKey(entityIds, range);\n if (store.cache.has(key)) return; // already warm\n fetchCalendarRange(getHass(), entityIds, range)\n .then((result) => writeCache(store.cache, key, result))\n .catch(() => {\n // best-effort prefetch; ignore failures\n });\n });\n\n const debouncedRefetch = useCallbackStable(() => {\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n debounceTimerRef.current = setTimeout(() => refetch(), 500);\n });\n\n useEffect(() => {\n const unsubscribes = entityIds.map((entityId) =>\n store.subscribeToEntity(entityId, debouncedRefetch),\n );\n return () => {\n unsubscribes.forEach((unsub) => unsub());\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n };\n }, [entityIdsKey, store.subscribeToEntity, debouncedRefetch]);\n\n return { events, status, error, refetch, prefetch };\n}\n\n/**\n * Event payload for the calendar mutation WebSocket commands. Dates are either\n * date-only strings (\"2026-07-17\", all-day) or ISO datetimes.\n */\nexport interface CalendarMutationEvent {\n dtstart: string;\n dtend: string;\n summary: string;\n description?: string;\n location?: string;\n rrule?: string;\n}\n\nfunction requireConnection(hass: HomeAssistant | undefined): HomeAssistant {\n if (!hass?.connection) {\n throw new Error('Home Assistant connection not available');\n }\n return hass;\n}\n\n/**\n * Create an event on a calendar that supports mutation (e.g. Local Calendar).\n * Requires entity control permission, not admin. WS errors reject unchanged so\n * callers can inspect `err.code` (e.g. 'unauthorized').\n */\nexport async function createCalendarEvent(\n hass: HomeAssistant | undefined,\n entityId: `calendar.${string}`,\n event: CalendarMutationEvent,\n): Promise<void> {\n await requireConnection(hass).connection.sendMessagePromise({\n type: 'calendar/event/create',\n entity_id: entityId,\n event,\n });\n}\n\n/** Delete a calendar event by its uid. */\nexport async function deleteCalendarEvent(\n hass: HomeAssistant | undefined,\n entityId: `calendar.${string}`,\n uid: string,\n opts?: { recurrenceId?: string; recurrenceRange?: string },\n): Promise<void> {\n await requireConnection(hass).connection.sendMessagePromise({\n type: 'calendar/event/delete',\n entity_id: entityId,\n uid,\n ...(opts?.recurrenceId !== undefined && { recurrence_id: opts.recurrenceId }),\n ...(opts?.recurrenceRange !== undefined && { recurrence_range: opts.recurrenceRange }),\n });\n}\n\n/** Replace a calendar event's content by its uid. */\nexport async function updateCalendarEvent(\n hass: HomeAssistant | undefined,\n entityId: `calendar.${string}`,\n uid: string,\n event: CalendarMutationEvent,\n): Promise<void> {\n await requireConnection(hass).connection.sendMessagePromise({\n type: 'calendar/event/update',\n entity_id: entityId,\n uid,\n event,\n });\n}\n","import type { RefObject } from 'preact';\nimport { useEffect, useRef } from 'preact/hooks';\n\nexport interface ElementSize {\n width: number;\n height: number;\n}\n\nexport type ResizeCallback = (size: ElementSize) => void;\n\n/**\n * Observe an element's size via ResizeObserver. The callback fires:\n *\n * 1. Once after mount, with the element's current size.\n * 2. Whenever the element's size changes.\n * 3. Whenever `deps` change, re-firing with the current size — so callers\n * can re-run draws when their inputs change without re-creating the\n * observer.\n *\n * The callback is suppressed only while the element is detached from the\n * document. Zero width/height is delivered to the callback as-is — consumers\n * that need to skip degenerate sizes (e.g. canvas painters where a 0-sized\n * drawImage throws InvalidStateError) should add their own early return.\n *\n * Sizes are read from `offsetWidth` / `offsetHeight` (CSS pixels, includes\n * padding + border). The callback is held in a ref, so passing a fresh\n * function each render is safe — it never re-creates the observer.\n *\n * @example\n * const containerRef = useRef<HTMLDivElement>(null);\n * const canvasRef = useRef<HTMLCanvasElement>(null);\n *\n * useResizeObserver(\n * containerRef,\n * ({ width, height }) => {\n * if (width === 0 || height === 0) return; // optional, consumer's call\n * drawChart(canvasRef.current, forecast, width, height);\n * },\n * [forecast],\n * );\n */\nexport function useResizeObserver<T extends HTMLElement>(\n ref: RefObject<T>,\n callback: ResizeCallback,\n deps: unknown[] = [],\n): void {\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n // Set up the observer once per element. ResizeObserver fires once\n // synchronously-ish after `.observe()` with the current size, which\n // covers the initial draw.\n useEffect(() => {\n const element = ref.current;\n if (!element) return;\n\n const fire = () => {\n if (!element.isConnected) return;\n callbackRef.current({\n width: element.offsetWidth,\n height: element.offsetHeight,\n });\n };\n\n const observer = new ResizeObserver(fire);\n observer.observe(element);\n return () => observer.disconnect();\n // ref identity is stable across renders; observer setup runs once.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Re-fire on dependency change. Skip the first render — the observer's\n // initial `.observe()` fire already delivers the mount-time size.\n const isFirstRun = useRef(true);\n useEffect(() => {\n if (isFirstRun.current) {\n isFirstRun.current = false;\n return;\n }\n const element = ref.current;\n if (!element || !element.isConnected) return;\n callbackRef.current({\n width: element.offsetWidth,\n height: element.offsetHeight,\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n}\n","import type { RefObject } from 'preact';\nimport { useState } from 'preact/hooks';\nimport { useResizeObserver } from './useResizeObserver';\n\n/**\n * Track the current width of a referenced element in CSS pixels.\n *\n * Returns `undefined` until the first non-zero measurement is observed, then\n * a positive number that updates as the element resizes. Once a real width\n * is captured the hook will never report `undefined` or `0` again, even\n * during HA layout transitions (dashboard switch, edit-mode toggle) — the\n * underlying ResizeObserver firings are silently dropped while the element\n * is detached or transiently zero-width, so the component renders with the\n * last good value instead of flashing through a degenerate state.\n *\n * Use this when you need a width value in JSX (responsive layout, prop to a\n * sized child). If you only need the value imperatively inside a draw\n * callback, prefer `useResizeObserver` directly — it doesn't allocate\n * component state or cause re-renders.\n *\n * @example\n * const ref = useRef<HTMLDivElement>(null);\n * const width = useWidth(ref);\n * return (\n * <div ref={ref}>\n * {width !== undefined && <Chart width={width} />}\n * </div>\n * );\n */\nexport function useWidth<T extends HTMLElement>(ref: RefObject<T>): number | undefined {\n const [width, setWidth] = useState<number | undefined>(undefined);\n useResizeObserver(ref, (size) => {\n if (size.width === 0) return;\n setWidth((prev) => (prev === size.width ? prev : size.width));\n });\n return width;\n}\n"],"names":["HACard"],"mappings":"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"}
|