custom-card-helpers 2.0.0 → 2.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.
Files changed (41) hide show
  1. package/.github/dependabot.yml +13 -0
  2. package/.github/workflows/claude-code-review.yml +44 -0
  3. package/.github/workflows/claude.yml +50 -0
  4. package/.github/workflows/release.yml +4 -0
  5. package/.github/workflows/test.yml +1 -0
  6. package/dist/compute-name.d.ts +7 -0
  7. package/dist/compute-state-display.d.ts +1 -2
  8. package/dist/datetime/duration_to_seconds.d.ts +1 -1
  9. package/dist/datetime/seconds_to_duration.d.ts +1 -1
  10. package/dist/datetime/select_unit.d.ts +8 -0
  11. package/dist/get-lovelace.d.ts +5 -1
  12. package/dist/index.js +259 -23
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.m.js +254 -23
  15. package/dist/index.m.js.map +1 -1
  16. package/dist/index.umd.js +263 -25
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/scroll-to-target.d.ts +1 -1
  19. package/dist/translations/localize.d.ts +1 -2
  20. package/dist/turn-on-off-entity.d.ts +2 -0
  21. package/dist/types.d.ts +156 -4
  22. package/package.json +7 -8
  23. package/src/compute-name.ts +7 -0
  24. package/src/compute-state-display.ts +2 -3
  25. package/src/cover_icon.ts +74 -6
  26. package/src/datetime/duration_to_seconds.ts +1 -1
  27. package/src/datetime/relative_time.ts +1 -2
  28. package/src/datetime/seconds_to_duration.ts +1 -1
  29. package/src/datetime/select_unit.ts +94 -0
  30. package/src/datetime/timer_time_remaining.ts +1 -1
  31. package/src/get-lovelace.ts +22 -16
  32. package/src/handle-action.ts +19 -2
  33. package/src/handle-click.ts +1 -1
  34. package/src/scroll-to-target.ts +1 -1
  35. package/src/state_icon.ts +1 -1
  36. package/src/translations/localize.ts +1 -3
  37. package/src/turn-on-off-entity.ts +10 -0
  38. package/src/types.ts +203 -3
  39. package/test/datetime.test.ts +2 -2
  40. package/test/icons.test.ts +51 -0
  41. package/test/service-actions.test.ts +19 -1
@@ -23,3 +23,13 @@ export const turnOnOffEntity = (
23
23
 
24
24
  return hass.callService(serviceDomain, service, { entity_id: entityId });
25
25
  };
26
+
27
+ export const turnOnEntity = (
28
+ hass: HomeAssistant,
29
+ entityId: string
30
+ ): Promise<void> => turnOnOffEntity(hass, entityId, true);
31
+
32
+ export const turnOffEntity = (
33
+ hass: HomeAssistant,
34
+ entityId: string
35
+ ): Promise<void> => turnOnOffEntity(hass, entityId, false);
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  HassEntities,
3
+ HassEntity,
3
4
  HassConfig,
4
5
  Auth,
5
6
  Connection,
@@ -8,6 +9,37 @@ import {
8
9
  HassServiceTarget,
9
10
  } from "home-assistant-js-websocket";
10
11
  import { HapticType } from "./haptic";
12
+
13
+ export interface DeviceRegistryEntry {
14
+ id: string;
15
+ name: string | null;
16
+ name_by_user: string | null;
17
+ area_id: string | null;
18
+ /** Set when this device is a sub-device of another. @since Home Assistant 2026.4 */
19
+ parent_device_id?: string | null;
20
+ }
21
+
22
+ export interface AreaRegistryEntry {
23
+ area_id: string;
24
+ name: string;
25
+ floor_id: string | null;
26
+ }
27
+
28
+ export interface FloorRegistryEntry {
29
+ floor_id: string;
30
+ name: string;
31
+ level: number | null;
32
+ }
33
+
34
+ /** One part of a composed entity name, as accepted by `formatEntityName`. */
35
+ export type EntityNameItem =
36
+ | { type: "entity" | "device" | "area" | "floor" }
37
+ | { type: "text"; text: string };
38
+
39
+ export interface EntityNameOptions {
40
+ /** Joins the resolved parts. Defaults to a single space. */
41
+ separator?: string;
42
+ }
11
43
  import { HASSDomEvent } from "./fire-event";
12
44
 
13
45
  export interface ToggleMenuActionConfig extends BaseActionConfig {
@@ -21,7 +53,7 @@ export interface ToggleActionConfig extends BaseActionConfig {
21
53
  export interface CallServiceActionConfig extends BaseActionConfig {
22
54
  action: "call-service";
23
55
  service: string;
24
- service_data?: {
56
+ data?: {
25
57
  entity_id?: string | [string];
26
58
  [key: string]: any;
27
59
  };
@@ -54,6 +86,16 @@ export interface CustomActionConfig extends BaseActionConfig {
54
86
  action: "fire-dom-event";
55
87
  }
56
88
 
89
+ export interface TurnOnActionConfig extends BaseActionConfig {
90
+ action: "turn_on";
91
+ entity?: string;
92
+ }
93
+
94
+ export interface TurnOffActionConfig extends BaseActionConfig {
95
+ action: "turn_off";
96
+ entity?: string;
97
+ }
98
+
57
99
  /**
58
100
  * `repeat` and `haptic` are specifically for use in custom cards like the Button-Card
59
101
  */
@@ -81,7 +123,18 @@ export type ActionConfig =
81
123
  | MoreInfoActionConfig
82
124
  | NoActionConfig
83
125
  | CustomActionConfig
84
- | ToggleMenuActionConfig;
126
+ | ToggleMenuActionConfig
127
+ | TurnOnActionConfig
128
+ | TurnOffActionConfig;
129
+
130
+ export interface HuiRootElement extends HTMLElement {
131
+ lovelace: {
132
+ config: LovelaceConfig;
133
+ current_view: number;
134
+ [key: string]: any;
135
+ };
136
+ ___curView: number;
137
+ }
85
138
 
86
139
  export interface Window {
87
140
  // Custom panel entry point url
@@ -187,6 +240,21 @@ export interface ServiceCallRequest {
187
240
  target?: HassServiceTarget;
188
241
  }
189
242
 
243
+ // Entry of the entity registry as exposed by the HA frontend in `hass.entities`.
244
+ export interface EntityRegistryDisplayEntry {
245
+ entity_id: string;
246
+ name?: string;
247
+ icon?: string;
248
+ device_id?: string;
249
+ area_id?: string;
250
+ labels: string[];
251
+ hidden?: boolean;
252
+ entity_category?: "config" | "diagnostic";
253
+ translation_key?: string;
254
+ platform?: string;
255
+ display_precision?: number;
256
+ }
257
+
190
258
  export interface HomeAssistant {
191
259
  auth: Auth;
192
260
  connection: Connection;
@@ -218,6 +286,47 @@ export interface HomeAssistant {
218
286
  };
219
287
  };
220
288
 
289
+ // Registry data. Populated over the websocket connection after connect, and
290
+ // needed to resolve an entity's naming context. All optional: `floors` only
291
+ // exists from HA 2024.4, and marking the rest optional keeps this interface
292
+ // safe to construct in tests and mocks.
293
+ /** Entity registry keyed by entity_id. */
294
+ entities?: Record<string, EntityRegistryDisplayEntry>;
295
+ devices?: Record<string, DeviceRegistryEntry>;
296
+ areas?: Record<string, AreaRegistryEntry>;
297
+ floors?: Record<string, FloorRegistryEntry>;
298
+
299
+ // Display formatters, which apply the user's locale, precision and naming
300
+ // preferences the same way the built-in cards do. Optional because each was
301
+ // added in a specific Home Assistant version - check before calling, or gate
302
+ // on `hass.config.version`.
303
+ /** @since Home Assistant 2024.4 */
304
+ formatEntityState?: (stateObj: HassEntity, state?: string) => string;
305
+ /** @since Home Assistant 2024.4 */
306
+ formatEntityAttributeName?: (stateObj: HassEntity, attribute: string) => string;
307
+ /** @since Home Assistant 2024.4 */
308
+ formatEntityAttributeValue?: (
309
+ stateObj: HassEntity,
310
+ attribute: string,
311
+ value?: unknown
312
+ ) => string;
313
+ /**
314
+ * Formats an entity's display name from its registry context.
315
+ *
316
+ * Accepts a plain string (returned as-is), a single name item, an array of
317
+ * items joined with the separator, or `undefined` for the default name.
318
+ *
319
+ * @since Home Assistant 2026.4 in this shape. The helper exists from 2025.10,
320
+ * but took bare type strings then, and only accepted `EntityNameItem`s
321
+ * between 2025.11 and 2026.3 - so feature detection is not sufficient, check
322
+ * `hass.config.version` before calling with a card's `name` option.
323
+ */
324
+ formatEntityName?: (
325
+ stateObj: HassEntity,
326
+ name?: string | EntityNameItem | EntityNameItem[],
327
+ options?: EntityNameOptions
328
+ ) => string;
329
+
221
330
  dockedSidebar: boolean;
222
331
  moreInfoEntityId: string;
223
332
  user: CurrentUser;
@@ -238,6 +347,7 @@ export interface HomeAssistant {
238
347
  ) => Promise<Response>;
239
348
  sendWS: (msg: MessageBase) => Promise<void>;
240
349
  callWS: <T>(msg: MessageBase) => Promise<T>;
350
+ hassUrl: (path?: string) => string;
241
351
  }
242
352
 
243
353
  export enum NumberFormat {
@@ -256,10 +366,37 @@ export enum TimeFormat {
256
366
  twenty_four = "24",
257
367
  }
258
368
 
369
+ export enum TimeZone {
370
+ local = "local",
371
+ server = "server",
372
+ }
373
+
374
+ export enum DateFormat {
375
+ language = "language",
376
+ system = "system",
377
+ DMY = "DMY",
378
+ MDY = "MDY",
379
+ YMD = "YMD",
380
+ }
381
+
382
+ export enum FirstWeekday {
383
+ language = "language",
384
+ monday = "monday",
385
+ tuesday = "tuesday",
386
+ wednesday = "wednesday",
387
+ thursday = "thursday",
388
+ friday = "friday",
389
+ saturday = "saturday",
390
+ sunday = "sunday",
391
+ }
392
+
259
393
  export interface FrontendLocaleData {
260
394
  language: string;
261
395
  number_format: NumberFormat;
262
396
  time_format: TimeFormat;
397
+ date_format: DateFormat;
398
+ first_weekday: FirstWeekday;
399
+ time_zone: TimeZone;
263
400
  }
264
401
 
265
402
  export interface LovelaceCardConfig {
@@ -283,6 +420,69 @@ export interface LovelaceCardEditor extends HTMLElement {
283
420
  setConfig(config: LovelaceCardConfig): void;
284
421
  }
285
422
 
423
+ export interface LovelaceCardFeatureConfig {
424
+ type: string;
425
+ [key: string]: any;
426
+ }
427
+
428
+ export type LovelaceCardFeaturePosition = "bottom" | "inline";
429
+
430
+ export interface LovelaceCardFeatureContext {
431
+ entity_id?: string;
432
+ area_id?: string;
433
+ }
434
+
435
+ export interface LovelaceCardFeature extends HTMLElement {
436
+ hass?: HomeAssistant;
437
+ /** @deprecated Use `context` instead */
438
+ stateObj?: HassEntity;
439
+ context?: LovelaceCardFeatureContext;
440
+ setConfig(config: LovelaceCardFeatureConfig): void;
441
+ color?: string;
442
+ position?: LovelaceCardFeaturePosition;
443
+ }
444
+
445
+ export interface LovelaceCardFeatureEditor extends HTMLElement {
446
+ hass?: HomeAssistant;
447
+ lovelace?: LovelaceConfig;
448
+ context?: LovelaceCardFeatureContext;
449
+ setConfig(config: LovelaceCardFeatureConfig): void;
450
+ }
451
+
452
+ // Loose description of a ha-form schema item
453
+ export interface HaFormSchema {
454
+ name?: string;
455
+ type?: string;
456
+ [key: string]: any;
457
+ }
458
+
459
+ // Static side of a card feature class, e.g. `MyFeature satisfies LovelaceCardFeatureConstructor`
460
+ export interface LovelaceCardFeatureConstructor {
461
+ new (...args: any[]): LovelaceCardFeature;
462
+ getStubConfig?: (
463
+ hass: HomeAssistant,
464
+ context?: LovelaceCardFeatureContext
465
+ ) => LovelaceCardFeatureConfig;
466
+ getConfigElement?: () => LovelaceCardFeatureEditor;
467
+ getConfigForm?: () => {
468
+ schema: HaFormSchema[];
469
+ assertConfig?: (config: LovelaceCardConfig) => void;
470
+ };
471
+ }
472
+
473
+ // Entry for `window.customCardFeatures`, shown in the card feature picker
474
+ export interface CustomCardFeatureEntry {
475
+ type: string;
476
+ name?: string;
477
+ configurable?: boolean;
478
+ isSupported?: (
479
+ hass: HomeAssistant,
480
+ context: LovelaceCardFeatureContext
481
+ ) => boolean;
482
+ /** @deprecated Use `isSupported` instead */
483
+ supported?: (stateObj: HassEntity) => boolean;
484
+ }
485
+
286
486
  export interface LovelaceConfig {
287
487
  title?: string;
288
488
  views: LovelaceViewConfig[];
@@ -339,7 +539,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
339
539
  | "brightness";
340
540
  action_name?: string;
341
541
  service?: string;
342
- service_data?: Record<string, unknown>;
542
+ data?: Record<string, unknown>;
343
543
  url?: string;
344
544
  tap_action?: ActionConfig;
345
545
  hold_action?: ActionConfig;
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import durationToSeconds from "../src/datetime/duration_to_seconds";
3
- import secondsToDuration from "../src/datetime/seconds_to_duration";
2
+ import { durationToSeconds } from "../src/datetime/duration_to_seconds";
3
+ import { secondsToDuration } from "../src/datetime/seconds_to_duration";
4
4
  import { timerTimeRemaining } from "../src/datetime/timer_time_remaining";
5
5
  import { useAmPm } from "../src/datetime/use_am_pm";
6
6
  import { TimeFormat } from "../src/types";
@@ -4,6 +4,7 @@ import { coverIcon } from "../src/cover_icon";
4
4
  import { domainIcon } from "../src/domain_icons";
5
5
  import { inputDateTimeIcon } from "../src/input_datetime_icon";
6
6
  import { sensorIcon } from "../src/sensor_icon";
7
+ import { stateIcon } from "../src/state_icon";
7
8
 
8
9
  describe("domainIcon", () => {
9
10
  it("returns fixed icons and state-based icons", () => {
@@ -54,6 +55,36 @@ describe("coverIcon", () => {
54
55
  coverIcon({ state: "closed", attributes: { device_class: "garage" } } as any)
55
56
  ).toBe("mdi:garage");
56
57
  });
58
+
59
+ const icon = (state: string, device_class?: string) =>
60
+ coverIcon({ state, attributes: { device_class } } as any);
61
+
62
+ it("returns transitional icons while opening and closing", () => {
63
+ expect(icon("opening", "garage")).toBe("mdi:arrow-up-box");
64
+ expect(icon("closing", "garage")).toBe("mdi:arrow-down-box");
65
+ expect(icon("opening", "gate")).toBe("mdi:gate-arrow-right");
66
+ expect(icon("opening", "curtain")).toBe("mdi:arrow-split-vertical");
67
+ expect(icon("closing", "curtain")).toBe("mdi:arrow-collapse-horizontal");
68
+ expect(icon("opening", "shade")).toBe("mdi:arrow-up-box");
69
+ });
70
+
71
+ it("returns closed and open icons for newly supported classes", () => {
72
+ expect(icon("closed", "gate")).toBe("mdi:gate");
73
+ expect(icon("open", "gate")).toBe("mdi:gate-open");
74
+ expect(icon("closed", "shutter")).toBe("mdi:window-shutter");
75
+ expect(icon("open", "curtain")).toBe("mdi:curtains");
76
+ expect(icon("closed", "curtain")).toBe("mdi:curtains-closed");
77
+ expect(icon("closed", "blind")).toBe("mdi:blinds");
78
+ expect(icon("open", "damper")).toBe("mdi:circle");
79
+ expect(icon("closed", "damper")).toBe("mdi:circle-slice-8");
80
+ });
81
+
82
+ it("falls back to window icons for unknown device classes", () => {
83
+ expect(icon("closed")).toBe("mdi:window-closed");
84
+ expect(icon("open")).toBe("mdi:window-open");
85
+ expect(icon("opening")).toBe("mdi:arrow-up-box");
86
+ expect(icon("closing")).toBe("mdi:arrow-down-box");
87
+ });
57
88
  });
58
89
 
59
90
  describe("inputDateTimeIcon", () => {
@@ -69,3 +100,23 @@ describe("inputDateTimeIcon", () => {
69
100
  );
70
101
  });
71
102
  });
103
+
104
+ describe("stateIcon", () => {
105
+ it("returns correct icon for binary_sensor with device_class", () => {
106
+ const stateObj = {
107
+ entity_id: "binary_sensor.door",
108
+ state: "off",
109
+ attributes: { device_class: "door" },
110
+ } as any;
111
+ expect(stateIcon(stateObj)).toBe("mdi:door-closed");
112
+ });
113
+
114
+ it("returns correct icon for binary_sensor on state", () => {
115
+ const stateObj = {
116
+ entity_id: "binary_sensor.door",
117
+ state: "on",
118
+ attributes: { device_class: "door" },
119
+ } as any;
120
+ expect(stateIcon(stateObj)).toBe("mdi:door-open");
121
+ });
122
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { toggleEntity } from "../src/toggle-entity";
3
3
  import { turnOnOffEntities } from "../src/turn-on-off-entities";
4
- import { turnOnOffEntity } from "../src/turn-on-off-entity";
4
+ import { turnOnOffEntity, turnOnEntity, turnOffEntity } from "../src/turn-on-off-entity";
5
5
 
6
6
  describe("turnOnOffEntity", () => {
7
7
  it("uses domain-specific lock and cover services", async () => {
@@ -46,6 +46,24 @@ describe("toggleEntity", () => {
46
46
  });
47
47
  });
48
48
 
49
+ describe("turnOnEntity / turnOffEntity", () => {
50
+ it("turnOnEntity calls turn_on service", async () => {
51
+ const callService = vi.fn().mockResolvedValue(undefined);
52
+ await turnOnEntity({ callService } as any, "scene.movie_night");
53
+ expect(callService).toHaveBeenCalledWith("scene", "turn_on", {
54
+ entity_id: "scene.movie_night",
55
+ });
56
+ });
57
+
58
+ it("turnOffEntity calls turn_off service", async () => {
59
+ const callService = vi.fn().mockResolvedValue(undefined);
60
+ await turnOffEntity({ callService } as any, "switch.kitchen");
61
+ expect(callService).toHaveBeenCalledWith("switch", "turn_off", {
62
+ entity_id: "switch.kitchen",
63
+ });
64
+ });
65
+ });
66
+
49
67
  describe("turnOnOffEntities", () => {
50
68
  it("batches service calls by service domain", () => {
51
69
  const callService = vi.fn();