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.
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/release.yml +4 -0
- package/.github/workflows/test.yml +1 -0
- package/dist/compute-name.d.ts +7 -0
- package/dist/compute-state-display.d.ts +1 -2
- package/dist/datetime/duration_to_seconds.d.ts +1 -1
- package/dist/datetime/seconds_to_duration.d.ts +1 -1
- package/dist/datetime/select_unit.d.ts +8 -0
- package/dist/get-lovelace.d.ts +5 -1
- package/dist/index.js +259 -23
- package/dist/index.js.map +1 -1
- package/dist/index.m.js +254 -23
- package/dist/index.m.js.map +1 -1
- package/dist/index.umd.js +263 -25
- package/dist/index.umd.js.map +1 -1
- package/dist/scroll-to-target.d.ts +1 -1
- package/dist/translations/localize.d.ts +1 -2
- package/dist/turn-on-off-entity.d.ts +2 -0
- package/dist/types.d.ts +156 -4
- package/package.json +7 -8
- package/src/compute-name.ts +7 -0
- package/src/compute-state-display.ts +2 -3
- package/src/cover_icon.ts +74 -6
- package/src/datetime/duration_to_seconds.ts +1 -1
- package/src/datetime/relative_time.ts +1 -2
- package/src/datetime/seconds_to_duration.ts +1 -1
- package/src/datetime/select_unit.ts +94 -0
- package/src/datetime/timer_time_remaining.ts +1 -1
- package/src/get-lovelace.ts +22 -16
- package/src/handle-action.ts +19 -2
- package/src/handle-click.ts +1 -1
- package/src/scroll-to-target.ts +1 -1
- package/src/state_icon.ts +1 -1
- package/src/translations/localize.ts +1 -3
- package/src/turn-on-off-entity.ts +10 -0
- package/src/types.ts +203 -3
- package/test/datetime.test.ts +2 -2
- package/test/icons.test.ts +51 -0
- package/test/service-actions.test.ts +19 -1
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { HomeAssistant } from "./types";
|
|
2
2
|
export declare const turnOnOffEntity: (hass: HomeAssistant, entityId: string, turnOn?: boolean) => Promise<void>;
|
|
3
|
+
export declare const turnOnEntity: (hass: HomeAssistant, entityId: string) => Promise<void>;
|
|
4
|
+
export declare const turnOffEntity: (hass: HomeAssistant, entityId: string) => Promise<void>;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
|
-
import { HassEntities, HassConfig, Auth, Connection, MessageBase, HassServices, HassServiceTarget } from "home-assistant-js-websocket";
|
|
1
|
+
import { HassEntities, HassEntity, HassConfig, Auth, Connection, MessageBase, HassServices, HassServiceTarget } from "home-assistant-js-websocket";
|
|
2
2
|
import { HapticType } from "./haptic";
|
|
3
|
+
export interface DeviceRegistryEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string | null;
|
|
6
|
+
name_by_user: string | null;
|
|
7
|
+
area_id: string | null;
|
|
8
|
+
/** Set when this device is a sub-device of another. @since Home Assistant 2026.4 */
|
|
9
|
+
parent_device_id?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface AreaRegistryEntry {
|
|
12
|
+
area_id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
floor_id: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface FloorRegistryEntry {
|
|
17
|
+
floor_id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
level: number | null;
|
|
20
|
+
}
|
|
21
|
+
/** One part of a composed entity name, as accepted by `formatEntityName`. */
|
|
22
|
+
export type EntityNameItem = {
|
|
23
|
+
type: "entity" | "device" | "area" | "floor";
|
|
24
|
+
} | {
|
|
25
|
+
type: "text";
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
export interface EntityNameOptions {
|
|
29
|
+
/** Joins the resolved parts. Defaults to a single space. */
|
|
30
|
+
separator?: string;
|
|
31
|
+
}
|
|
3
32
|
import { HASSDomEvent } from "./fire-event";
|
|
4
33
|
export interface ToggleMenuActionConfig extends BaseActionConfig {
|
|
5
34
|
action: "toggle-menu";
|
|
@@ -10,7 +39,7 @@ export interface ToggleActionConfig extends BaseActionConfig {
|
|
|
10
39
|
export interface CallServiceActionConfig extends BaseActionConfig {
|
|
11
40
|
action: "call-service";
|
|
12
41
|
service: string;
|
|
13
|
-
|
|
42
|
+
data?: {
|
|
14
43
|
entity_id?: string | [string];
|
|
15
44
|
[key: string]: any;
|
|
16
45
|
};
|
|
@@ -36,6 +65,14 @@ export interface NoActionConfig extends BaseActionConfig {
|
|
|
36
65
|
export interface CustomActionConfig extends BaseActionConfig {
|
|
37
66
|
action: "fire-dom-event";
|
|
38
67
|
}
|
|
68
|
+
export interface TurnOnActionConfig extends BaseActionConfig {
|
|
69
|
+
action: "turn_on";
|
|
70
|
+
entity?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface TurnOffActionConfig extends BaseActionConfig {
|
|
73
|
+
action: "turn_off";
|
|
74
|
+
entity?: string;
|
|
75
|
+
}
|
|
39
76
|
/**
|
|
40
77
|
* `repeat` and `haptic` are specifically for use in custom cards like the Button-Card
|
|
41
78
|
*/
|
|
@@ -52,7 +89,15 @@ export interface ConfirmationRestrictionConfig {
|
|
|
52
89
|
export interface RestrictionConfig {
|
|
53
90
|
user: string;
|
|
54
91
|
}
|
|
55
|
-
export type ActionConfig = ToggleActionConfig | CallServiceActionConfig | NavigateActionConfig | UrlActionConfig | MoreInfoActionConfig | NoActionConfig | CustomActionConfig | ToggleMenuActionConfig;
|
|
92
|
+
export type ActionConfig = ToggleActionConfig | CallServiceActionConfig | NavigateActionConfig | UrlActionConfig | MoreInfoActionConfig | NoActionConfig | CustomActionConfig | ToggleMenuActionConfig | TurnOnActionConfig | TurnOffActionConfig;
|
|
93
|
+
export interface HuiRootElement extends HTMLElement {
|
|
94
|
+
lovelace: {
|
|
95
|
+
config: LovelaceConfig;
|
|
96
|
+
current_view: number;
|
|
97
|
+
[key: string]: any;
|
|
98
|
+
};
|
|
99
|
+
___curView: number;
|
|
100
|
+
}
|
|
56
101
|
export interface Window {
|
|
57
102
|
customPanelJS: string;
|
|
58
103
|
ShadyCSS: {
|
|
@@ -147,6 +192,19 @@ export interface ServiceCallRequest {
|
|
|
147
192
|
serviceData?: Record<string, any>;
|
|
148
193
|
target?: HassServiceTarget;
|
|
149
194
|
}
|
|
195
|
+
export interface EntityRegistryDisplayEntry {
|
|
196
|
+
entity_id: string;
|
|
197
|
+
name?: string;
|
|
198
|
+
icon?: string;
|
|
199
|
+
device_id?: string;
|
|
200
|
+
area_id?: string;
|
|
201
|
+
labels: string[];
|
|
202
|
+
hidden?: boolean;
|
|
203
|
+
entity_category?: "config" | "diagnostic";
|
|
204
|
+
translation_key?: string;
|
|
205
|
+
platform?: string;
|
|
206
|
+
display_precision?: number;
|
|
207
|
+
}
|
|
150
208
|
export interface HomeAssistant {
|
|
151
209
|
auth: Auth;
|
|
152
210
|
connection: Connection;
|
|
@@ -169,6 +227,29 @@ export interface HomeAssistant {
|
|
|
169
227
|
[lang: string]: Translation;
|
|
170
228
|
};
|
|
171
229
|
};
|
|
230
|
+
/** Entity registry keyed by entity_id. */
|
|
231
|
+
entities?: Record<string, EntityRegistryDisplayEntry>;
|
|
232
|
+
devices?: Record<string, DeviceRegistryEntry>;
|
|
233
|
+
areas?: Record<string, AreaRegistryEntry>;
|
|
234
|
+
floors?: Record<string, FloorRegistryEntry>;
|
|
235
|
+
/** @since Home Assistant 2024.4 */
|
|
236
|
+
formatEntityState?: (stateObj: HassEntity, state?: string) => string;
|
|
237
|
+
/** @since Home Assistant 2024.4 */
|
|
238
|
+
formatEntityAttributeName?: (stateObj: HassEntity, attribute: string) => string;
|
|
239
|
+
/** @since Home Assistant 2024.4 */
|
|
240
|
+
formatEntityAttributeValue?: (stateObj: HassEntity, attribute: string, value?: unknown) => string;
|
|
241
|
+
/**
|
|
242
|
+
* Formats an entity's display name from its registry context.
|
|
243
|
+
*
|
|
244
|
+
* Accepts a plain string (returned as-is), a single name item, an array of
|
|
245
|
+
* items joined with the separator, or `undefined` for the default name.
|
|
246
|
+
*
|
|
247
|
+
* @since Home Assistant 2026.4 in this shape. The helper exists from 2025.10,
|
|
248
|
+
* but took bare type strings then, and only accepted `EntityNameItem`s
|
|
249
|
+
* between 2025.11 and 2026.3 - so feature detection is not sufficient, check
|
|
250
|
+
* `hass.config.version` before calling with a card's `name` option.
|
|
251
|
+
*/
|
|
252
|
+
formatEntityName?: (stateObj: HassEntity, name?: string | EntityNameItem | EntityNameItem[], options?: EntityNameOptions) => string;
|
|
172
253
|
dockedSidebar: boolean;
|
|
173
254
|
moreInfoEntityId: string;
|
|
174
255
|
user: CurrentUser;
|
|
@@ -181,6 +262,7 @@ export interface HomeAssistant {
|
|
|
181
262
|
}) => Promise<Response>;
|
|
182
263
|
sendWS: (msg: MessageBase) => Promise<void>;
|
|
183
264
|
callWS: <T>(msg: MessageBase) => Promise<T>;
|
|
265
|
+
hassUrl: (path?: string) => string;
|
|
184
266
|
}
|
|
185
267
|
export declare enum NumberFormat {
|
|
186
268
|
language = "language",
|
|
@@ -196,10 +278,34 @@ export declare enum TimeFormat {
|
|
|
196
278
|
am_pm = "12",
|
|
197
279
|
twenty_four = "24"
|
|
198
280
|
}
|
|
281
|
+
export declare enum TimeZone {
|
|
282
|
+
local = "local",
|
|
283
|
+
server = "server"
|
|
284
|
+
}
|
|
285
|
+
export declare enum DateFormat {
|
|
286
|
+
language = "language",
|
|
287
|
+
system = "system",
|
|
288
|
+
DMY = "DMY",
|
|
289
|
+
MDY = "MDY",
|
|
290
|
+
YMD = "YMD"
|
|
291
|
+
}
|
|
292
|
+
export declare enum FirstWeekday {
|
|
293
|
+
language = "language",
|
|
294
|
+
monday = "monday",
|
|
295
|
+
tuesday = "tuesday",
|
|
296
|
+
wednesday = "wednesday",
|
|
297
|
+
thursday = "thursday",
|
|
298
|
+
friday = "friday",
|
|
299
|
+
saturday = "saturday",
|
|
300
|
+
sunday = "sunday"
|
|
301
|
+
}
|
|
199
302
|
export interface FrontendLocaleData {
|
|
200
303
|
language: string;
|
|
201
304
|
number_format: NumberFormat;
|
|
202
305
|
time_format: TimeFormat;
|
|
306
|
+
date_format: DateFormat;
|
|
307
|
+
first_weekday: FirstWeekday;
|
|
308
|
+
time_zone: TimeZone;
|
|
203
309
|
}
|
|
204
310
|
export interface LovelaceCardConfig {
|
|
205
311
|
index?: number;
|
|
@@ -219,6 +325,52 @@ export interface LovelaceCardEditor extends HTMLElement {
|
|
|
219
325
|
lovelace?: LovelaceConfig;
|
|
220
326
|
setConfig(config: LovelaceCardConfig): void;
|
|
221
327
|
}
|
|
328
|
+
export interface LovelaceCardFeatureConfig {
|
|
329
|
+
type: string;
|
|
330
|
+
[key: string]: any;
|
|
331
|
+
}
|
|
332
|
+
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
|
333
|
+
export interface LovelaceCardFeatureContext {
|
|
334
|
+
entity_id?: string;
|
|
335
|
+
area_id?: string;
|
|
336
|
+
}
|
|
337
|
+
export interface LovelaceCardFeature extends HTMLElement {
|
|
338
|
+
hass?: HomeAssistant;
|
|
339
|
+
/** @deprecated Use `context` instead */
|
|
340
|
+
stateObj?: HassEntity;
|
|
341
|
+
context?: LovelaceCardFeatureContext;
|
|
342
|
+
setConfig(config: LovelaceCardFeatureConfig): void;
|
|
343
|
+
color?: string;
|
|
344
|
+
position?: LovelaceCardFeaturePosition;
|
|
345
|
+
}
|
|
346
|
+
export interface LovelaceCardFeatureEditor extends HTMLElement {
|
|
347
|
+
hass?: HomeAssistant;
|
|
348
|
+
lovelace?: LovelaceConfig;
|
|
349
|
+
context?: LovelaceCardFeatureContext;
|
|
350
|
+
setConfig(config: LovelaceCardFeatureConfig): void;
|
|
351
|
+
}
|
|
352
|
+
export interface HaFormSchema {
|
|
353
|
+
name?: string;
|
|
354
|
+
type?: string;
|
|
355
|
+
[key: string]: any;
|
|
356
|
+
}
|
|
357
|
+
export interface LovelaceCardFeatureConstructor {
|
|
358
|
+
new (...args: any[]): LovelaceCardFeature;
|
|
359
|
+
getStubConfig?: (hass: HomeAssistant, context?: LovelaceCardFeatureContext) => LovelaceCardFeatureConfig;
|
|
360
|
+
getConfigElement?: () => LovelaceCardFeatureEditor;
|
|
361
|
+
getConfigForm?: () => {
|
|
362
|
+
schema: HaFormSchema[];
|
|
363
|
+
assertConfig?: (config: LovelaceCardConfig) => void;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
export interface CustomCardFeatureEntry {
|
|
367
|
+
type: string;
|
|
368
|
+
name?: string;
|
|
369
|
+
configurable?: boolean;
|
|
370
|
+
isSupported?: (hass: HomeAssistant, context: LovelaceCardFeatureContext) => boolean;
|
|
371
|
+
/** @deprecated Use `isSupported` instead */
|
|
372
|
+
supported?: (stateObj: HassEntity) => boolean;
|
|
373
|
+
}
|
|
222
374
|
export interface LovelaceConfig {
|
|
223
375
|
title?: string;
|
|
224
376
|
views: LovelaceViewConfig[];
|
|
@@ -261,7 +413,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
|
|
|
261
413
|
secondary_info?: "entity-id" | "last-changed" | "last-triggered" | "last-updated" | "position" | "tilt-position" | "brightness";
|
|
262
414
|
action_name?: string;
|
|
263
415
|
service?: string;
|
|
264
|
-
|
|
416
|
+
data?: Record<string, unknown>;
|
|
265
417
|
url?: string;
|
|
266
418
|
tap_action?: ActionConfig;
|
|
267
419
|
hold_action?: ActionConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "custom-card-helpers",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Set of helpful functions and types for Custom Card creators",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"umd:main": "dist/index.umd.js",
|
|
@@ -37,9 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/custom-cards/custom-card-helpers#readme",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@formatjs/intl-utils": "^3.8.4",
|
|
41
40
|
"home-assistant-js-websocket": "^9.6.0",
|
|
42
|
-
"intl-messageformat": "^
|
|
41
|
+
"intl-messageformat": "^12.1.5",
|
|
43
42
|
"superstruct": "^2.0.2"
|
|
44
43
|
},
|
|
45
44
|
"peerDependencies": {
|
|
@@ -47,12 +46,12 @@
|
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
50
|
-
"jsdom": "^
|
|
49
|
+
"jsdom": "^31.0.0",
|
|
51
50
|
"lit": "^3.3.1",
|
|
52
|
-
"rollup": "^4.
|
|
51
|
+
"rollup": "^4.60.0",
|
|
53
52
|
"tslib": "^2.8.1",
|
|
54
|
-
"typedoc": "^0.28.
|
|
53
|
+
"typedoc": "^0.28.20",
|
|
55
54
|
"typescript": "^5.9.3",
|
|
56
|
-
"vitest": "^
|
|
55
|
+
"vitest": "^4.1.0"
|
|
57
56
|
}
|
|
58
|
-
}
|
|
57
|
+
}
|
package/src/compute-name.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { HassEntity } from "home-assistant-js-websocket";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Reads `friendly_name`, which Home Assistant is moving away from -
|
|
5
|
+
* it now always includes the device name, so this duplicates context the
|
|
6
|
+
* surrounding UI already shows. Prefer `hass.formatEntityName(stateObj, name)`
|
|
7
|
+
* on HA 2026.4 and later, which resolves the name from the entity's registry
|
|
8
|
+
* context and lets users choose which parts to include.
|
|
9
|
+
*/
|
|
3
10
|
export const computeName = (stateObj: HassEntity): string =>
|
|
4
11
|
stateObj.attributes.friendly_name || stateObj.entity_id;
|
|
@@ -2,9 +2,8 @@ import { HassEntity } from "home-assistant-js-websocket";
|
|
|
2
2
|
import { formatDateTime } from "./datetime/format_date_time";
|
|
3
3
|
import { formatDate } from "./datetime/format_date";
|
|
4
4
|
import { formatTime } from "./datetime/format_time";
|
|
5
|
-
import { LocalizeFunc } from "./translations/localize";
|
|
6
5
|
import { computeStateDomain } from "./compute-state-domain";
|
|
7
|
-
import { FrontendLocaleData } from "./types";
|
|
6
|
+
import { FrontendLocaleData, LocalizeFunc } from "./types";
|
|
8
7
|
import { formatNumber, isNumericState } from "./format-number";
|
|
9
8
|
|
|
10
9
|
export const computeStateDisplay = (
|
|
@@ -126,4 +125,4 @@ export const computeStateDisplay = (
|
|
|
126
125
|
// We don't know! Return the raw state.
|
|
127
126
|
compareState
|
|
128
127
|
);
|
|
129
|
-
};
|
|
128
|
+
};
|
package/src/cover_icon.ts
CHANGED
|
@@ -1,21 +1,89 @@
|
|
|
1
1
|
/** Return an icon representing a cover state. */
|
|
2
2
|
import { HassEntity } from "home-assistant-js-websocket";
|
|
3
|
-
import { domainIcon } from "./domain_icons";
|
|
4
3
|
|
|
5
4
|
export const coverIcon = (state: HassEntity): string => {
|
|
6
5
|
const open = state.state !== "closed";
|
|
7
6
|
switch (state.attributes.device_class) {
|
|
8
7
|
case "garage":
|
|
9
|
-
|
|
8
|
+
switch (state.state) {
|
|
9
|
+
case "opening":
|
|
10
|
+
return "mdi:arrow-up-box";
|
|
11
|
+
case "closing":
|
|
12
|
+
return "mdi:arrow-down-box";
|
|
13
|
+
case "closed":
|
|
14
|
+
return "mdi:garage";
|
|
15
|
+
default:
|
|
16
|
+
return "mdi:garage-open";
|
|
17
|
+
}
|
|
18
|
+
case "gate":
|
|
19
|
+
switch (state.state) {
|
|
20
|
+
case "opening":
|
|
21
|
+
case "closing":
|
|
22
|
+
return "mdi:gate-arrow-right";
|
|
23
|
+
case "closed":
|
|
24
|
+
return "mdi:gate";
|
|
25
|
+
default:
|
|
26
|
+
return "mdi:gate-open";
|
|
27
|
+
}
|
|
10
28
|
case "door":
|
|
11
29
|
return open ? "mdi:door-open" : "mdi:door-closed";
|
|
30
|
+
case "damper":
|
|
31
|
+
return open ? "mdi:circle" : "mdi:circle-slice-8";
|
|
12
32
|
case "shutter":
|
|
13
|
-
|
|
33
|
+
switch (state.state) {
|
|
34
|
+
case "opening":
|
|
35
|
+
return "mdi:arrow-up-box";
|
|
36
|
+
case "closing":
|
|
37
|
+
return "mdi:arrow-down-box";
|
|
38
|
+
case "closed":
|
|
39
|
+
return "mdi:window-shutter";
|
|
40
|
+
default:
|
|
41
|
+
return "mdi:window-shutter-open";
|
|
42
|
+
}
|
|
43
|
+
case "curtain":
|
|
44
|
+
switch (state.state) {
|
|
45
|
+
case "opening":
|
|
46
|
+
return "mdi:arrow-split-vertical";
|
|
47
|
+
case "closing":
|
|
48
|
+
return "mdi:arrow-collapse-horizontal";
|
|
49
|
+
case "closed":
|
|
50
|
+
return "mdi:curtains-closed";
|
|
51
|
+
default:
|
|
52
|
+
return "mdi:curtains";
|
|
53
|
+
}
|
|
14
54
|
case "blind":
|
|
15
|
-
|
|
55
|
+
case "shade":
|
|
56
|
+
switch (state.state) {
|
|
57
|
+
case "opening":
|
|
58
|
+
return "mdi:arrow-up-box";
|
|
59
|
+
case "closing":
|
|
60
|
+
return "mdi:arrow-down-box";
|
|
61
|
+
case "closed":
|
|
62
|
+
return "mdi:blinds";
|
|
63
|
+
default:
|
|
64
|
+
return "mdi:blinds-open";
|
|
65
|
+
}
|
|
16
66
|
case "window":
|
|
17
|
-
|
|
67
|
+
switch (state.state) {
|
|
68
|
+
case "opening":
|
|
69
|
+
return "mdi:arrow-up-box";
|
|
70
|
+
case "closing":
|
|
71
|
+
return "mdi:arrow-down-box";
|
|
72
|
+
case "closed":
|
|
73
|
+
return "mdi:window-closed";
|
|
74
|
+
default:
|
|
75
|
+
return "mdi:window-open";
|
|
76
|
+
}
|
|
18
77
|
default:
|
|
19
|
-
|
|
78
|
+
switch (state.state) {
|
|
79
|
+
case "opening":
|
|
80
|
+
return "mdi:arrow-up-box";
|
|
81
|
+
case "closing":
|
|
82
|
+
return "mdi:arrow-down-box";
|
|
83
|
+
case "closed":
|
|
84
|
+
return "mdi:window-closed";
|
|
85
|
+
default:
|
|
86
|
+
return "mdi:window-open";
|
|
87
|
+
}
|
|
20
88
|
}
|
|
21
89
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @param duration hh:mm:ss formated duration
|
|
4
4
|
* @returns duration in seconds
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
6
|
+
export function durationToSeconds(duration: string): number {
|
|
7
7
|
const parts = duration.split(":").map(Number);
|
|
8
8
|
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
9
9
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
//REF: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/relative_time.ts
|
|
2
2
|
|
|
3
|
-
import { selectUnit } from "@formatjs/intl-utils";
|
|
4
3
|
import { FrontendLocaleData } from "../types";
|
|
4
|
+
import { selectUnit } from "./select_unit";
|
|
5
5
|
|
|
6
6
|
const formatRelTimeMem =
|
|
7
7
|
(locale: FrontendLocaleData) =>
|
|
8
8
|
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" });
|
|
9
|
-
|
|
10
9
|
/**
|
|
11
10
|
* Calculate a string representing a date object as relative time from now.
|
|
12
11
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export function secondsToDuration(d: number) {
|
|
6
6
|
const h = Math.floor(d / 3600);
|
|
7
7
|
const m = Math.floor((d % 3600) / 60);
|
|
8
8
|
const s = Math.floor((d % 3600) % 60);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* selectUnit() taken from @formatjs/intl-units 3.8.4.
|
|
3
|
+
*
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Unit =
|
|
7
|
+
| 'second'
|
|
8
|
+
| 'minute'
|
|
9
|
+
| 'hour'
|
|
10
|
+
| 'day'
|
|
11
|
+
| 'week'
|
|
12
|
+
| 'month'
|
|
13
|
+
| 'quarter'
|
|
14
|
+
| 'year';
|
|
15
|
+
|
|
16
|
+
const MS_PER_SECOND = 1e3;
|
|
17
|
+
const SECS_PER_MIN = 60;
|
|
18
|
+
const SECS_PER_HOUR = SECS_PER_MIN * 60;
|
|
19
|
+
const SECS_PER_DAY = SECS_PER_HOUR * 24;
|
|
20
|
+
const SECS_PER_WEEK = SECS_PER_DAY * 7;
|
|
21
|
+
|
|
22
|
+
export function selectUnit(
|
|
23
|
+
from: Date | number,
|
|
24
|
+
to: Date | number = Date.now(),
|
|
25
|
+
thresholds: Partial<Thresholds> = {}
|
|
26
|
+
): {value: number; unit: Unit} {
|
|
27
|
+
const resolvedThresholds: Thresholds = {
|
|
28
|
+
...DEFAULT_THRESHOLDS,
|
|
29
|
+
...(thresholds || {}),
|
|
30
|
+
};
|
|
31
|
+
const secs = (+from - +to) / MS_PER_SECOND;
|
|
32
|
+
if (Math.abs(secs) < resolvedThresholds.second) {
|
|
33
|
+
return {
|
|
34
|
+
value: Math.round(secs),
|
|
35
|
+
unit: 'second',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const mins = secs / SECS_PER_MIN;
|
|
39
|
+
if (Math.abs(mins) < resolvedThresholds.minute) {
|
|
40
|
+
return {
|
|
41
|
+
value: Math.round(mins),
|
|
42
|
+
unit: 'minute',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const hours = secs / SECS_PER_HOUR;
|
|
46
|
+
if (Math.abs(hours) < resolvedThresholds.hour) {
|
|
47
|
+
return {
|
|
48
|
+
value: Math.round(hours),
|
|
49
|
+
unit: 'hour',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const days = secs / SECS_PER_DAY;
|
|
54
|
+
if (Math.abs(days) < resolvedThresholds.day) {
|
|
55
|
+
return {
|
|
56
|
+
value: Math.round(days),
|
|
57
|
+
unit: 'day',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const fromDate = new Date(from);
|
|
61
|
+
const toDate = new Date(to);
|
|
62
|
+
|
|
63
|
+
const years = fromDate.getFullYear() - toDate.getFullYear();
|
|
64
|
+
if (Math.round(Math.abs(years)) > 0) {
|
|
65
|
+
return {
|
|
66
|
+
value: Math.round(years),
|
|
67
|
+
unit: 'year',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const months = years * 12 + fromDate.getMonth() - toDate.getMonth();
|
|
72
|
+
if (Math.round(Math.abs(months)) > 0) {
|
|
73
|
+
return {
|
|
74
|
+
value: Math.round(months),
|
|
75
|
+
unit: 'month',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const weeks = secs / SECS_PER_WEEK;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
value: Math.round(weeks),
|
|
82
|
+
unit: 'week',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type Thresholds = Record<'second' | 'minute' | 'hour' | 'day', number>;
|
|
87
|
+
|
|
88
|
+
export const DEFAULT_THRESHOLDS: Thresholds = {
|
|
89
|
+
second: 45, // seconds to minute
|
|
90
|
+
minute: 45, // minutes to hour
|
|
91
|
+
hour: 22, // hour to day
|
|
92
|
+
day: 5, // day to week
|
|
93
|
+
};
|
|
94
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HassEntity } from "home-assistant-js-websocket";
|
|
2
|
-
import durationToSeconds from "../datetime/duration_to_seconds";
|
|
2
|
+
import { durationToSeconds } from "../datetime/duration_to_seconds";
|
|
3
3
|
|
|
4
4
|
export function timerTimeRemaining(stateObj: HassEntity) {
|
|
5
5
|
let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
|
package/src/get-lovelace.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { HuiRootElement } from "./types";
|
|
2
|
+
|
|
1
3
|
export const getLovelace = () => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
root
|
|
8
|
-
root
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
const root = document
|
|
5
|
+
.querySelector("home-assistant")
|
|
6
|
+
?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot;
|
|
7
|
+
|
|
8
|
+
const resolver =
|
|
9
|
+
root?.querySelector("ha-drawer partial-panel-resolver") ||
|
|
10
|
+
root?.querySelector("app-drawer-layout partial-panel-resolver");
|
|
11
|
+
|
|
12
|
+
const huiRoot = (resolver?.shadowRoot || resolver)
|
|
13
|
+
?.querySelector<HuiRootElement>("ha-panel-lovelace")
|
|
14
|
+
?.shadowRoot?.querySelector<HuiRootElement>("hui-root");
|
|
15
|
+
|
|
16
|
+
if (huiRoot) {
|
|
17
|
+
const ll = huiRoot.lovelace;
|
|
18
|
+
ll.current_view = huiRoot.___curView;
|
|
19
|
+
return ll;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
};
|
package/src/handle-action.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { HomeAssistant, ActionConfig } from "./types";
|
|
1
|
+
import { HomeAssistant, ActionConfig, TurnOnActionConfig, TurnOffActionConfig } from "./types";
|
|
2
2
|
import { forwardHaptic } from "./haptic";
|
|
3
3
|
import { fireEvent } from "./fire-event";
|
|
4
4
|
import { navigate } from "./navigate";
|
|
5
5
|
import { toggleEntity } from "./toggle-entity";
|
|
6
|
+
import { turnOnOffEntity } from "./turn-on-off-entity";
|
|
6
7
|
|
|
7
8
|
export const handleActionConfig = (
|
|
8
9
|
node: HTMLElement,
|
|
@@ -65,13 +66,29 @@ export const handleActionConfig = (
|
|
|
65
66
|
forwardHaptic("success");
|
|
66
67
|
}
|
|
67
68
|
break;
|
|
69
|
+
case "turn_on":
|
|
70
|
+
turnOnOffEntity(
|
|
71
|
+
hass,
|
|
72
|
+
(actionConfig as TurnOnActionConfig).entity || config.entity!,
|
|
73
|
+
true
|
|
74
|
+
);
|
|
75
|
+
forwardHaptic("success");
|
|
76
|
+
break;
|
|
77
|
+
case "turn_off":
|
|
78
|
+
turnOnOffEntity(
|
|
79
|
+
hass,
|
|
80
|
+
(actionConfig as TurnOffActionConfig).entity || config.entity!,
|
|
81
|
+
false
|
|
82
|
+
);
|
|
83
|
+
forwardHaptic("success");
|
|
84
|
+
break;
|
|
68
85
|
case "call-service": {
|
|
69
86
|
if (!actionConfig.service) {
|
|
70
87
|
forwardHaptic("failure");
|
|
71
88
|
return;
|
|
72
89
|
}
|
|
73
90
|
const [domain, service] = actionConfig.service.split(".", 2);
|
|
74
|
-
hass.callService(domain, service, actionConfig.
|
|
91
|
+
hass.callService(domain, service, actionConfig.data, actionConfig.target);
|
|
75
92
|
forwardHaptic("success");
|
|
76
93
|
break;
|
|
77
94
|
}
|
package/src/handle-click.ts
CHANGED
|
@@ -84,7 +84,7 @@ export const handleClick = (
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
const [domain, service] = actionConfig.service.split(".", 2);
|
|
87
|
-
const serviceData = { ...actionConfig.
|
|
87
|
+
const serviceData = { ...actionConfig.data };
|
|
88
88
|
if (serviceData.entity_id === "entity") {
|
|
89
89
|
serviceData.entity_id = config.entity;
|
|
90
90
|
}
|
package/src/scroll-to-target.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @param {number} top The coordinate to scroll to, along the y-axis.
|
|
8
8
|
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export function scrollToTarget(element, target) {
|
|
11
11
|
// the scroll event will trigger _updateScrollState directly,
|
|
12
12
|
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
|
|
13
13
|
// Calling _updateScrollState will ensure that the states are synced correctly.
|
package/src/state_icon.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { inputDateTimeIcon } from "./input_datetime_icon";
|
|
|
8
8
|
import { domainIcon } from "./domain_icons";
|
|
9
9
|
|
|
10
10
|
const domainIcons = {
|
|
11
|
-
binary_sensor: binarySensorIcon,
|
|
11
|
+
binary_sensor: (stateObj: HassEntity) => binarySensorIcon(stateObj.state, stateObj),
|
|
12
12
|
cover: coverIcon,
|
|
13
13
|
sensor: sensorIcon,
|
|
14
14
|
input_datetime: inputDateTimeIcon,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
//REF: https://github.com/home-assistant/frontend/blob/dev/src/common/translations/localize.ts
|
|
2
2
|
|
|
3
3
|
import IntlMessageFormat from "intl-messageformat";
|
|
4
|
-
import { Resources } from "../types";
|
|
5
|
-
|
|
6
|
-
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
|
4
|
+
import { LocalizeFunc, Resources } from "../types";
|
|
7
5
|
|
|
8
6
|
interface FormatType {
|
|
9
7
|
[format: string]: any;
|