preact-homeassistant 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stu Kabakoff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # preact-homeassistant
2
+
3
+ Preact hooks and helpers for building Home Assistant custom cards. Handles the
4
+ web-component lifecycle, Shadow DOM, entity subscriptions, and data fetching so
5
+ you can focus on your card's UI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add preact preact-homeassistant
11
+ ```
12
+
13
+ `preact` is a peer dependency.
14
+
15
+ ## Quick start
16
+
17
+ ```tsx
18
+ import { registerPreactCard, useEntity, css } from 'preact-homeassistant';
19
+
20
+ css`
21
+ .my-card { padding: 16px; }
22
+ .my-card .temperature { font-size: 2em; }
23
+ `;
24
+
25
+ function MyCardContent({ config }: { config: { entity: string } }) {
26
+ const weather = useEntity(config.entity);
27
+
28
+ return (
29
+ <ha-card>
30
+ <div class="card-content my-card">
31
+ <span class="temperature">{weather?.state ?? '...'}</span>
32
+ </div>
33
+ </ha-card>
34
+ );
35
+ }
36
+
37
+ function MyCardEditor({ hass, config, onConfigChanged }) {
38
+ const entities = Object.keys(hass.states).filter((e) => e.startsWith('weather.'));
39
+
40
+ return (
41
+ <div style={{ padding: '16px' }}>
42
+ <ha-select
43
+ label="Weather entity"
44
+ value={config.entity}
45
+ naturalMenuWidth
46
+ fixedMenuPosition
47
+ onChange={(e) => onConfigChanged({ ...config, entity: (e.target as HTMLSelectElement).value })}
48
+ onclosed={(e) => e.stopPropagation()}
49
+ >
50
+ {entities.map((id) => (
51
+ <ha-list-item key={id} value={id}>
52
+ {hass.states[id]?.attributes?.friendly_name ?? id}
53
+ </ha-list-item>
54
+ ))}
55
+ </ha-select>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ registerPreactCard({
61
+ type: 'my-weather-card',
62
+ name: 'My Weather Card',
63
+ description: 'A simple weather card',
64
+ Component: MyCardContent,
65
+ ConfigComponent: MyCardEditor,
66
+ getStubConfig: () => ({ entity: '' }),
67
+ });
68
+ ```
69
+
70
+ That's it. `registerPreactCard` creates the web component, registers the custom
71
+ element with Home Assistant, sets up Shadow DOM, injects registered styles, and
72
+ wraps your component in the data provider. Your component receives `config` as a
73
+ prop and uses hooks for everything else.
74
+
75
+ ## `registerPreactCard(options)`
76
+
77
+ | Option | Type | Required | Description |
78
+ |---|---|---|---|
79
+ | `type` | `string` | Yes | Custom element tag name (e.g. `'my-weather-card'`) |
80
+ | `name` | `string` | Yes | Display name in the HA card picker |
81
+ | `description` | `string` | Yes | Description in the HA card picker |
82
+ | `Component` | `ComponentType<{ config: T }>` | Yes | Main card Preact component |
83
+ | `ConfigComponent` | `ComponentType<{ hass, config, onConfigChanged }>` | No | Visual editor. Receives `hass`, the current `config`, and an `onConfigChanged` callback. Registered as `${type}-editor`. |
84
+ | `UnconfiguredComponent` | `ComponentType<{}>` | No | Shown before config/hass are available |
85
+ | `getStubConfig` | `() => Partial<T>` | No | Default config for the card picker |
86
+
87
+ The card renders into a Shadow DOM root. The editor renders into the light DOM
88
+ (required for HA's own custom elements like `<ha-select>` to work).
89
+
90
+ ## Hooks
91
+
92
+ ### `useEntity(entityId)`
93
+
94
+ Subscribe to a specific entity. Only re-renders when that entity's state changes.
95
+
96
+ ```tsx
97
+ const sensor = useEntity('sensor.temperature');
98
+ // sensor?.state === '72'
99
+ ```
100
+
101
+ Returns a strict type based on the domain prefix:
102
+ - `'calendar.*'` → `CalendarEntity`
103
+ - `'weather.*'` → `WeatherEntity`
104
+ - `'sun.sun'` → `SunEntity`
105
+ - Other domains → `HassEntity` (the loose type from `home-assistant-js-websocket`)
106
+
107
+ The mapping comes from the `DomainEntityMap` interface. To add a new domain,
108
+ see the *Contributing types* section below.
109
+
110
+ ### `useHass()`
111
+
112
+ Access the full `hass` object for calling services or reading config. Does not
113
+ re-render on entity changes.
114
+
115
+ ```tsx
116
+ const { getHass } = useHass();
117
+ await getHass()?.callService('light', 'turn_on', { entity_id: 'light.bedroom' });
118
+ ```
119
+
120
+ ### `useCalendarEvents(entityId, { start, end })`
121
+
122
+ Fetch calendar events for a date range from a single calendar.
123
+
124
+ ### `useMultiCalendarEvents(entityIds, { start, end })`
125
+
126
+ Fetch events from multiple calendars. Events are returned with `calendarId`
127
+ attached. Caches to localStorage and debounce-refetches when entities change.
128
+
129
+ ```tsx
130
+ const { events, status, error, refetch } = useMultiCalendarEvents(
131
+ ['calendar.family', 'calendar.work'],
132
+ { start, end },
133
+ );
134
+ // status: 'loading' | 'cached' | 'ready' | 'refreshing'
135
+ ```
136
+
137
+ ### `useWeatherForecast(entityId, type)`
138
+
139
+ Fetch weather forecast data. Caches to localStorage, debounce-refetches on
140
+ entity changes, and auto-refetches at the top of each hour.
141
+
142
+ ```tsx
143
+ const { forecast, status, error, refetch } = useWeatherForecast('weather.home', 'hourly');
144
+ ```
145
+
146
+ ### `useCachedFetch(cacheKey, fetcher, deps)`
147
+
148
+ Generic hook for fetching data with localStorage caching. The domain-specific
149
+ hooks above are built on this.
150
+
151
+ ## Styles
152
+
153
+ Styles are registered globally via the `css\`\`` tagged template and
154
+ auto-injected into each card's Shadow DOM by `registerPreactCard`. Use
155
+ `.styles.ts` files imported as side effects.
156
+
157
+ ```tsx
158
+ // MyCard.styles.ts
159
+ import { css } from 'preact-homeassistant';
160
+
161
+ css`
162
+ .my-card { padding: 16px; }
163
+ `;
164
+
165
+ // MyCard.tsx
166
+ import './MyCard.styles'; // registers styles on import
167
+ ```
168
+
169
+ ### `registerRawStyles(cssString)`
170
+
171
+ Register a raw CSS string, e.g. from a Vite `?inline` import.
172
+
173
+ ## Cache utilities
174
+
175
+ `loadFromCache(key)` / `saveToCache(key, data)` — localStorage wrapper with
176
+ 24-hour expiry. Used internally by the data hooks.
177
+
178
+ ## Other utilities
179
+
180
+ ### `useCallbackStable(fn)`
181
+
182
+ Returns a stable callback ref that always calls the latest `fn`. Avoids effect
183
+ re-runs while keeping the closure current.
184
+
185
+ ## Types
186
+
187
+ All HA domain types live in [`src/types/`](src/types/):
188
+
189
+ - [`calendar.ts`](src/types/calendar.ts) — `CalendarEntity`, `CalendarEvent`, `CalendarEventWithSource`
190
+ - [`weather.ts`](src/types/weather.ts) — `WeatherEntity`, `WeatherForecast`, `ForecastType`
191
+ - [`sun.ts`](src/types/sun.ts) — `SunEntity`
192
+ - [`common.ts`](src/types/common.ts) — `HomeAssistant`, `FetchStatus`
193
+ - [`index.ts`](src/types/index.ts) — `DomainEntityMap`, `EntityForId<T>`
194
+
195
+ Re-exported from the package root:
196
+
197
+ ```ts
198
+ import type {
199
+ HomeAssistant,
200
+ CalendarEntity,
201
+ WeatherEntity,
202
+ SunEntity,
203
+ WeatherForecast,
204
+ EntityForId,
205
+ /* ... */
206
+ } from 'preact-homeassistant';
207
+ ```
208
+
209
+ ## Contributing types
210
+
211
+ The HA domain types in this package are intentionally minimal — only the
212
+ domains the maintainers have actually needed. If your card needs strict types
213
+ for another domain (light, climate, media_player, cover, etc.), PRs are very
214
+ welcome.
215
+
216
+ 1. Look up the domain in the [Home Assistant frontend repo](https://github.com/home-assistant/frontend/tree/dev/src/data) — most domains have a `data/<domain>.ts` file with TypeScript types.
217
+ 2. Add `src/types/<domain>.ts` mirroring the fields your card needs. Extend `HassEntityBase` and `HassEntityAttributeBase` from `home-assistant-js-websocket`.
218
+ 3. Add the entity to `DomainEntityMap` in [`src/types/index.ts`](src/types/index.ts) and re-export the types.
219
+ 4. Add a quick test under `src/__tests__/` if you're feeling thorough.
220
+ 5. PR.
221
+
222
+ We err toward including only fields that are well-documented; speculative
223
+ attributes can land later.
224
+
225
+ ## Development
226
+
227
+ ```bash
228
+ pnpm install
229
+ pnpm test # vitest run
230
+ pnpm build # tsc --noEmit && vite build
231
+ pnpm lint # biome check
232
+ ```
233
+
234
+ ## Publishing
235
+
236
+ Releases are published to npm manually from a local machine (no CI publish):
237
+
238
+ ```bash
239
+ pnpm test && pnpm build && pnpm typecheck
240
+ git tag v0.X.Y && git push origin v0.X.Y
241
+ pnpm publish --access public --provenance
242
+ ```
243
+
244
+ The `--provenance` flag attaches SLSA build attestation. A GitHub release with
245
+ release notes + the packaged tarball is created automatically when the tag is
246
+ pushed (see [`.github/workflows/release.yml`](.github/workflows/release.yml)).
247
+
248
+ ## License
249
+
250
+ MIT
@@ -0,0 +1,276 @@
1
+ import { ComponentChildren } from 'preact';
2
+ import { ComponentType } from 'preact';
3
+ import { Connection } from 'home-assistant-js-websocket';
4
+ import { HassConfig } from 'home-assistant-js-websocket';
5
+ import { HassEntities } from 'home-assistant-js-websocket';
6
+ import { HassEntity } from 'home-assistant-js-websocket';
7
+ import { HassEntityAttributeBase } from 'home-assistant-js-websocket';
8
+ import { HassEntityBase } from 'home-assistant-js-websocket';
9
+ import { HassServices } from 'home-assistant-js-websocket';
10
+ import { JSX } from 'preact';
11
+
12
+ /**
13
+ * Calendar entity - only exposes the current/next event.
14
+ * Use useCalendarEvents() to fetch a list of events.
15
+ */
16
+ export declare interface CalendarEntity extends HassEntityBase {
17
+ entity_id: `calendar.${string}`;
18
+ state: 'on' | 'off' | 'unavailable' | 'unknown';
19
+ attributes: HassEntityAttributeBase & {
20
+ message?: string;
21
+ description?: string;
22
+ start_time?: string;
23
+ end_time?: string;
24
+ location?: string;
25
+ all_day?: boolean;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Calendar event returned by the calendar/get_events websocket call.
31
+ */
32
+ export declare interface CalendarEvent {
33
+ start: string;
34
+ end: string;
35
+ summary: string;
36
+ description?: string;
37
+ location?: string;
38
+ uid?: string;
39
+ recurrence_id?: string;
40
+ rrule?: string;
41
+ }
42
+
43
+ /**
44
+ * Calendar event with its source calendar entity ID attached. Used by
45
+ * useMultiCalendarEvents() to disambiguate events from multiple calendars.
46
+ */
47
+ export declare interface CalendarEventWithSource extends CalendarEvent {
48
+ calendarId: string;
49
+ }
50
+
51
+ /**
52
+ * CSS tagged template literal for syntax highlighting.
53
+ * Automatically registers the styles with the global registry.
54
+ */
55
+ export declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => string;
56
+
57
+ /**
58
+ * Map of known HA domains to their strict entity types. Contributors adding
59
+ * new domain types should add a new file under `src/types/` and extend this map.
60
+ */
61
+ export declare interface DomainEntityMap {
62
+ calendar: CalendarEntity;
63
+ weather: WeatherEntity;
64
+ sun: SunEntity;
65
+ }
66
+
67
+ /**
68
+ * Infers the strict entity type from an entity ID literal type.
69
+ *
70
+ * EntityForId<'calendar.family'> -> CalendarEntity
71
+ * EntityForId<'weather.home'> -> WeatherEntity
72
+ * EntityForId<'sensor.foo'> -> HassEntity (fallback)
73
+ */
74
+ export declare type EntityForId<T extends string> = T extends `${infer D}.${string}` ? D extends KnownDomain ? DomainEntityMap[D] : HassEntity : HassEntity;
75
+
76
+ export declare type FetchStatus = 'loading' | 'cached' | 'ready' | 'refreshing';
77
+
78
+ export declare type ForecastType = 'daily' | 'hourly' | 'twice_daily';
79
+
80
+ export declare function HAProvider({ hass, subscribeToEntity, children }: HAProviderProps): JSX.Element;
81
+
82
+ declare interface HAProviderProps {
83
+ hass: HomeAssistant | undefined;
84
+ subscribeToEntity: (entityId: string, callback: (entity: any) => void) => () => void;
85
+ children: ComponentChildren;
86
+ }
87
+
88
+ /**
89
+ * Subset of the Home Assistant `hass` object passed to custom cards.
90
+ */
91
+ export declare interface HomeAssistant {
92
+ states: HassEntities;
93
+ config: HassConfig;
94
+ services: HassServices;
95
+ connection: Connection;
96
+ callService: (domain: string, service: string, data?: object) => Promise<void>;
97
+ themes?: {
98
+ darkMode?: boolean;
99
+ theme?: string;
100
+ };
101
+ }
102
+
103
+ declare type KnownDomain = keyof DomainEntityMap;
104
+
105
+ export declare function loadFromCache<T>(key: string): T | undefined;
106
+
107
+ export declare function registerPreactCard<TConfig>(options: RegisterPreactCardOptions<TConfig>): void;
108
+
109
+ declare interface RegisterPreactCardOptions<TConfig> {
110
+ type: string;
111
+ name: string;
112
+ description: string;
113
+ Component: ComponentType<{
114
+ config: TConfig;
115
+ }>;
116
+ ConfigComponent?: ComponentType<{
117
+ hass: HomeAssistant;
118
+ config: TConfig;
119
+ onConfigChanged: (config: TConfig) => void;
120
+ }>;
121
+ UnconfiguredComponent?: ComponentType<{}>;
122
+ getStubConfig?: () => Partial<TConfig>;
123
+ }
124
+
125
+ /**
126
+ * Register raw CSS string (e.g., from ?inline imports).
127
+ * Only registers if not already present.
128
+ */
129
+ export declare function registerRawStyles(styles: string): void;
130
+
131
+ export declare function saveToCache<T>(key: string, data: T): void;
132
+
133
+ /**
134
+ * Sun entity - provides sunrise/sunset and elevation information.
135
+ */
136
+ export declare interface SunEntity extends HassEntityBase {
137
+ entity_id: 'sun.sun';
138
+ state: 'above_horizon' | 'below_horizon';
139
+ attributes: HassEntityAttributeBase & {
140
+ next_dawn?: string;
141
+ next_dusk?: string;
142
+ next_midnight?: string;
143
+ next_noon?: string;
144
+ next_rising?: string;
145
+ next_setting?: string;
146
+ elevation?: number;
147
+ azimuth?: number;
148
+ rising?: boolean;
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Generic hook for fetching data with localStorage caching. Returns a cache-aware
154
+ * status string to distinguish cached vs fresh data.
155
+ */
156
+ export declare function useCachedFetch<T>(cacheKey: string, fetcher: () => Promise<T>, deps: unknown[]): UseCachedFetchResult<T>;
157
+
158
+ declare interface UseCachedFetchResult<T> {
159
+ data: T | undefined;
160
+ status: FetchStatus;
161
+ error: Error | undefined;
162
+ refetch: () => void;
163
+ }
164
+
165
+ /**
166
+ * Fetch calendar events for a date range from a single calendar.
167
+ */
168
+ export declare function useCalendarEvents(entityId: `calendar.${string}`, options: {
169
+ start: Date;
170
+ end: Date;
171
+ }): UseCalendarEventsResult;
172
+
173
+ declare interface UseCalendarEventsResult {
174
+ events: CalendarEvent[] | undefined;
175
+ loading: boolean;
176
+ error: Error | undefined;
177
+ refetch: () => void;
178
+ }
179
+
180
+ /**
181
+ * Creates a stable callback reference that always calls the latest version of the callback.
182
+ * Unlike useCallback, this never changes identity, so it won't cause re-renders in children.
183
+ *
184
+ * @param callback The callback function to stabilize
185
+ * @returns A stable function reference that always calls the latest callback
186
+ */
187
+ export declare function useCallbackStable<T extends (...args: never[]) => unknown>(callback: T): T;
188
+
189
+ /**
190
+ * Subscribe to a specific entity by ID. Re-renders only when that entity changes.
191
+ *
192
+ * Returns a typed entity based on the domain prefix:
193
+ * - 'calendar.xyz' -> CalendarEntity
194
+ * - 'weather.xyz' -> WeatherEntity
195
+ * - 'sun.sun' -> SunEntity
196
+ * - other domains -> HassEntity (fallback)
197
+ */
198
+ export declare function useEntity<T extends string>(entityId: T): EntityForId<T> | undefined;
199
+
200
+ /**
201
+ * Get access to the full hass object for calling services / accessing config.
202
+ * Does NOT re-render on entity changes. Use useEntity for that.
203
+ */
204
+ export declare function useHass(): {
205
+ getHass: () => HomeAssistant | undefined;
206
+ };
207
+
208
+ /**
209
+ * Fetch events from multiple calendars for a date range, with localStorage
210
+ * caching. Events are tagged with their source calendar ID.
211
+ */
212
+ export declare function useMultiCalendarEvents(entityIds: `calendar.${string}`[], options: {
213
+ start: Date;
214
+ end: Date;
215
+ }): UseMultiCalendarEventsResult;
216
+
217
+ declare interface UseMultiCalendarEventsResult {
218
+ events: CalendarEventWithSource[] | undefined;
219
+ status: FetchStatus;
220
+ error: Error | undefined;
221
+ refetch: () => void;
222
+ }
223
+
224
+ /**
225
+ * Fetch weather forecast data with localStorage caching. Auto-refetches at the
226
+ * top of each hour and when the underlying entity changes (debounced).
227
+ */
228
+ export declare function useWeatherForecast(entityId: `weather.${string}`, type: ForecastType): UseWeatherForecastResult;
229
+
230
+ declare interface UseWeatherForecastResult {
231
+ forecast: WeatherForecast[] | undefined;
232
+ status: FetchStatus;
233
+ error: Error | undefined;
234
+ refetch: () => void;
235
+ }
236
+
237
+ /**
238
+ * Weather entity - current conditions only.
239
+ * Use useWeatherForecast() to fetch forecast data.
240
+ */
241
+ export declare interface WeatherEntity extends HassEntityBase {
242
+ entity_id: `weather.${string}`;
243
+ state: string;
244
+ attributes: HassEntityAttributeBase & {
245
+ temperature?: number;
246
+ apparent_temperature?: number;
247
+ dew_point?: number;
248
+ humidity?: number;
249
+ pressure?: number;
250
+ wind_speed?: number;
251
+ wind_gust_speed?: number;
252
+ wind_bearing?: number;
253
+ visibility?: number;
254
+ supported_features?: number;
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Weather forecast item returned by the weather/get_forecasts websocket call.
260
+ */
261
+ export declare interface WeatherForecast {
262
+ datetime: string;
263
+ condition?: string;
264
+ temperature?: number;
265
+ templow?: number;
266
+ precipitation_probability?: number;
267
+ precipitation?: number;
268
+ humidity?: number;
269
+ wind_speed?: number;
270
+ wind_bearing?: number;
271
+ cloud_coverage?: number;
272
+ uv_index?: number;
273
+ is_daytime?: boolean;
274
+ }
275
+
276
+ export { }