@digital-alchemy/hass 24.9.4 → 24.10.1

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 (89) hide show
  1. package/README.md +1 -1
  2. package/dist/extensions/call-proxy.extension.d.ts +1 -1
  3. package/dist/extensions/call-proxy.extension.js +4 -1
  4. package/dist/extensions/call-proxy.extension.js.map +1 -1
  5. package/dist/extensions/entity.extension.js +3 -0
  6. package/dist/extensions/entity.extension.js.map +1 -1
  7. package/dist/extensions/websocket-api.extension.js +5 -11
  8. package/dist/extensions/websocket-api.extension.js.map +1 -1
  9. package/dist/helpers/notify.helper.d.ts +2 -2
  10. package/package.json +16 -14
  11. package/scripts/mock-assistant.sh +5 -0
  12. package/scripts/run-e2e.sh +7 -0
  13. package/scripts/test.sh +2 -0
  14. package/src/dynamic.ts +4254 -0
  15. package/src/extensions/area.extension.ts +118 -0
  16. package/src/extensions/backup.extension.ts +63 -0
  17. package/src/extensions/call-proxy.extension.ts +122 -0
  18. package/src/extensions/config.extension.ts +119 -0
  19. package/src/extensions/conversation.extension.ts +46 -0
  20. package/src/extensions/device.extension.ts +56 -0
  21. package/src/extensions/entity.extension.ts +347 -0
  22. package/src/extensions/events.extension.ts +25 -0
  23. package/src/extensions/fetch-api.extension.ts +269 -0
  24. package/src/extensions/floor.extension.ts +76 -0
  25. package/src/extensions/id-by.extension.ts +157 -0
  26. package/src/extensions/index.ts +16 -0
  27. package/src/extensions/internal.extension.ts +145 -0
  28. package/src/extensions/label.extension.ts +83 -0
  29. package/src/extensions/reference.extension.ts +330 -0
  30. package/src/extensions/registry.extension.ts +44 -0
  31. package/src/extensions/websocket-api.extension.ts +551 -0
  32. package/src/extensions/zone.extension.ts +69 -0
  33. package/src/hass.module.ts +217 -0
  34. package/src/helpers/backup.helper.ts +11 -0
  35. package/src/helpers/constants.helper.ts +30 -0
  36. package/src/helpers/device.helper.ts +25 -0
  37. package/src/helpers/entity-state.helper.ts +171 -0
  38. package/src/helpers/features.helper.ts +580 -0
  39. package/src/helpers/fetch/calendar.ts +54 -0
  40. package/src/helpers/fetch/configuration.ts +75 -0
  41. package/src/helpers/fetch/index.ts +5 -0
  42. package/src/helpers/fetch/server-log.ts +28 -0
  43. package/src/helpers/fetch/service-list.ts +64 -0
  44. package/src/helpers/fetch/weather-forecasts.ts +86 -0
  45. package/src/helpers/fetch.helper.ts +328 -0
  46. package/src/helpers/id-by.helper.ts +53 -0
  47. package/src/helpers/index.ts +13 -0
  48. package/src/helpers/interfaces.helper.ts +340 -0
  49. package/src/helpers/manifest.helper.ts +0 -0
  50. package/src/helpers/notify.helper.ts +302 -0
  51. package/src/helpers/registry.ts +281 -0
  52. package/src/helpers/utility.helper.ts +147 -0
  53. package/src/helpers/websocket.helper.ts +117 -0
  54. package/src/index.ts +5 -0
  55. package/src/mock_assistant/extensions/area.extension.ts +62 -0
  56. package/src/mock_assistant/extensions/config.extension.ts +33 -0
  57. package/src/mock_assistant/extensions/device.extension.ts +44 -0
  58. package/src/mock_assistant/extensions/entity-registry.extension.ts +41 -0
  59. package/src/mock_assistant/extensions/entity.extension.ts +114 -0
  60. package/src/mock_assistant/extensions/events.extension.ts +37 -0
  61. package/src/mock_assistant/extensions/fetch.extension.ts +3 -0
  62. package/src/mock_assistant/extensions/fixtures.extension.ts +79 -0
  63. package/src/mock_assistant/extensions/floor.extension.ts +64 -0
  64. package/src/mock_assistant/extensions/index.ts +12 -0
  65. package/src/mock_assistant/extensions/label.extension.ts +64 -0
  66. package/src/mock_assistant/extensions/services.extension.ts +25 -0
  67. package/src/mock_assistant/extensions/websocket-api.extension.ts +84 -0
  68. package/src/mock_assistant/extensions/zone.extension.ts +65 -0
  69. package/src/mock_assistant/helpers/fixtures.ts +22 -0
  70. package/src/mock_assistant/helpers/index.ts +1 -0
  71. package/src/mock_assistant/index.ts +3 -0
  72. package/src/mock_assistant/main.ts +46 -0
  73. package/src/mock_assistant/mock-assistant.module.ts +90 -0
  74. package/src/quickboot.module.ts +23 -0
  75. package/src/testing/area.spec.ts +189 -0
  76. package/src/testing/backup.spec.ts +157 -0
  77. package/src/testing/config.spec.ts +188 -0
  78. package/src/testing/device.spec.ts +89 -0
  79. package/src/testing/entity.spec.ts +171 -0
  80. package/src/testing/events.spec.ts +78 -0
  81. package/src/testing/fetch-api.spec.ts +410 -0
  82. package/src/testing/fixtures.spec.ts +158 -0
  83. package/src/testing/floor.spec.ts +186 -0
  84. package/src/testing/id-by.spec.ts +140 -0
  85. package/src/testing/label.spec.ts +186 -0
  86. package/src/testing/ref-by.spec.ts +300 -0
  87. package/src/testing/websocket.spec.ts +63 -0
  88. package/src/testing/workflow.spec.ts +195 -0
  89. package/src/testing/zone.spec.ts +109 -0
@@ -0,0 +1,217 @@
1
+ import { CreateLibrary } from "@digital-alchemy/core";
2
+
3
+ import {
4
+ Area,
5
+ Backup,
6
+ CallProxy,
7
+ Configure,
8
+ Device,
9
+ EntityManager,
10
+ Events,
11
+ FetchAPI,
12
+ FetchInternals,
13
+ Floor,
14
+ IDByExtension,
15
+ Label,
16
+ ReferenceExtension,
17
+ Registry,
18
+ WebsocketAPI,
19
+ Zone,
20
+ } from "./extensions";
21
+
22
+ export const LIB_HASS = CreateLibrary({
23
+ configuration: {
24
+ /**
25
+ * Where to reach Home Assistant at
26
+ *
27
+ * Will auto detect inside an addon
28
+ */
29
+ BASE_URL: {
30
+ default: "http://homeassistant.local:8123",
31
+ description: "Url to reach Home Assistant at",
32
+ type: "string",
33
+ },
34
+
35
+ /**
36
+ * When adding new integrations, app will receive 1 update event for everything that changes.
37
+ * This can result in a flood of updates where only a single one is needed at the very end.
38
+ *
39
+ * This setting helps control that.
40
+ */
41
+ EVENT_DEBOUNCE_MS: {
42
+ default: 50,
43
+ description: "Debounce reactions to registry changes",
44
+ type: "number",
45
+ },
46
+
47
+ /**
48
+ * ## ACKNOWLEDGE ME
49
+ *
50
+ * Home Assistant **should** respond to all sent messages with a reply to confirm it was received.
51
+ *
52
+ * If this does not happen, then a warning will be emitted into the logs
53
+ */
54
+ EXPECT_RESPONSE_AFTER: {
55
+ default: 5,
56
+ description:
57
+ "If sendMessage was set to expect a response, a warning will be emitted after this delay if one is not received",
58
+ type: "number",
59
+ },
60
+
61
+ /**
62
+ * General purpose variable, adds delays to things when retrying
63
+ *
64
+ * > **NOTE**: this is best set to `0` for unit tests
65
+ */
66
+ RETRY_INTERVAL: {
67
+ default: 5,
68
+ description: "How often to retry connecting on connection failure (seconds)",
69
+ type: "number",
70
+ },
71
+
72
+ /**
73
+ * @internal
74
+ */
75
+ SOCKET_AVG_DURATION: {
76
+ default: 5,
77
+ description:
78
+ "How many seconds worth of requests to use in avg for math in REQ_PER_SEC calculations",
79
+ type: "number",
80
+ },
81
+
82
+ /**
83
+ * @internal
84
+ */
85
+ SOCKET_CRASH_REQUESTS_PER_SEC: {
86
+ default: 500,
87
+ description:
88
+ "Socket service will commit sudoku if more than this many outgoing messages are sent to Home Assistant in a second. Usually indicates runaway code",
89
+ type: "number",
90
+ },
91
+
92
+ /**
93
+ * @internal
94
+ */
95
+ SOCKET_WARN_REQUESTS_PER_SEC: {
96
+ default: 300,
97
+ description:
98
+ "Emit warnings if the home controller attempts to send more than X messages to Home Assistant inside of a second",
99
+ type: "number",
100
+ },
101
+
102
+ /**
103
+ * Long lived access token
104
+ */
105
+ TOKEN: {
106
+ description: "Long lived access token to Home Assistant",
107
+ required: true,
108
+ type: "string",
109
+ },
110
+
111
+ /**
112
+ * Intended to be provided via command line switch. Ex:
113
+ *
114
+ * ```bash
115
+ * $ node dist/main.js --validate-configuration
116
+ * ```
117
+ */
118
+ VALIDATE_CONFIGURATION: {
119
+ default: false,
120
+ description: "Validate the credentials then quit",
121
+ type: "boolean",
122
+ },
123
+ },
124
+ name: "hass",
125
+ // no internal dependency ones first
126
+ priorityInit: ["internals", "fetch", "socket"],
127
+ services: {
128
+ /**
129
+ * home assistant areas
130
+ */
131
+ area: Area,
132
+
133
+ /**
134
+ * home assistant backup interactions
135
+ */
136
+ backup: Backup,
137
+
138
+ /**
139
+ * general service calling interface
140
+ */
141
+ call: CallProxy,
142
+
143
+ /**
144
+ * internal tools
145
+ */
146
+ configure: Configure,
147
+
148
+ /**
149
+ * device interactions
150
+ */
151
+ device: Device,
152
+
153
+ /**
154
+ * retrieve and interact with home assistant entities
155
+ */
156
+ entity: EntityManager,
157
+
158
+ /**
159
+ * named event attachments
160
+ */
161
+ events: Events,
162
+
163
+ /**
164
+ * rest api commands
165
+ */
166
+ fetch: FetchAPI,
167
+
168
+ /**
169
+ * floors, like groups of areas
170
+ */
171
+ floor: Floor,
172
+
173
+ /**
174
+ * search for entity ids in a type safe way
175
+ */
176
+ idBy: IDByExtension,
177
+
178
+ /**
179
+ * @internal
180
+ */
181
+ internals: FetchInternals,
182
+
183
+ /**
184
+ * home assistant label interactions
185
+ */
186
+ label: Label,
187
+
188
+ /**
189
+ * obtain references to entities
190
+ */
191
+ refBy: ReferenceExtension,
192
+
193
+ /**
194
+ * interact with the home assistant registry
195
+ */
196
+ registry: Registry,
197
+
198
+ /**
199
+ * websocket interface
200
+ */
201
+ socket: WebsocketAPI,
202
+
203
+ /**
204
+ * zone interactions
205
+ */
206
+ zone: Zone,
207
+ },
208
+ });
209
+
210
+ declare module "@digital-alchemy/core" {
211
+ export interface LoadedModules {
212
+ /**
213
+ * tools for interacting with home assistant
214
+ */
215
+ hass: typeof LIB_HASS;
216
+ }
217
+ }
@@ -0,0 +1,11 @@
1
+ export interface HomeAssistantBackup {
2
+ date: string;
3
+ name: string;
4
+ path: string;
5
+ size: number;
6
+ slug: string;
7
+ }
8
+ export interface BackupResponse {
9
+ backing_up: boolean;
10
+ backups: HomeAssistantBackup[];
11
+ }
@@ -0,0 +1,30 @@
1
+ export const HASS_ENTITY = "HASS_ENTITY";
2
+ export const HASS_ENTITY_GROUP = "HASS_ENTITY_GROUP";
3
+ export const ALL_ENTITIES_UPDATED = "ALL_ENTITIES_UPDATED";
4
+ export const SOCKET_READY = "SOCKET_READY";
5
+
6
+ export enum HassSocketMessageTypes {
7
+ auth_required = "auth_required",
8
+ auth_ok = "auth_ok",
9
+ event = "event",
10
+ result = "result",
11
+ pong = "pong",
12
+ auth_invalid = "auth_invalid",
13
+ }
14
+
15
+ export const HOME_ASSISTANT_MODULE_CONFIGURATION = "HOME_ASSISTANT_MODULE_CONFIGURATION";
16
+
17
+ /**
18
+ * Required for label support, which is an automatic process at boot
19
+ *
20
+ * Will not make feature optional to support older hass versions
21
+ * Update your stuff
22
+ */
23
+ export const MIN_SUPPORTED_HASS_VERSION = "2024.4.0";
24
+ export const EARLY_ON_READY = 1;
25
+ export const ENTITY_REGISTRY_UPDATED = "ENTITY_REGISTRY_UPDATED";
26
+ export const AREA_REGISTRY_UPDATED = "AREA_REGISTRY_UPDATED";
27
+ export const LABEL_REGISTRY_UPDATED = "LABEL_REGISTRY_UPDATED";
28
+ export const FLOOR_REGISTRY_UPDATED = "FLOOR_REGISTRY_UPDATED";
29
+ export const DEVICE_REGISTRY_UPDATED = "DEVICE_REGISTRY_UPDATED";
30
+ export const ZONE_REGISTRY_UPDATED = "ZONE_REGISTRY_UPDATED";
@@ -0,0 +1,25 @@
1
+ import { TDeviceId } from "../dynamic";
2
+
3
+ export interface DeviceDetails {
4
+ area_id: null | string;
5
+ configuration_url: null | string;
6
+ config_entries: string[];
7
+ connections: Array<string[]>;
8
+ disabled_by: null;
9
+ entry_type: EntryType | null;
10
+ hw_version: null | string;
11
+ id: TDeviceId;
12
+ identifiers: Array<Array<number | string>>;
13
+ labels: string[];
14
+ manufacturer: null | string;
15
+ model: null | string;
16
+ name_by_user: null | string;
17
+ name: string;
18
+ serial_number: null;
19
+ sw_version: null | string;
20
+ via_device_id: TDeviceId;
21
+ }
22
+
23
+ export enum EntryType {
24
+ Service = "service",
25
+ }
@@ -0,0 +1,171 @@
1
+ import { FIRST, TBlackHole } from "@digital-alchemy/core";
2
+ import { Dayjs } from "dayjs";
3
+ import { Except } from "type-fest";
4
+
5
+ import { iCallService, TAreaId, TDeviceId, TLabelId, TPlatformId, TRawDomains } from "../dynamic";
6
+ import { SensorUnitOfMeasurement } from "./registry";
7
+ import {
8
+ ALL_DOMAINS,
9
+ ALL_SERVICE_DOMAINS,
10
+ ANY_ENTITY,
11
+ ENTITY_STATE,
12
+ GetDomain,
13
+ PICK_ENTITY,
14
+ } from "./utility.helper";
15
+
16
+ export interface HassEntityContext {
17
+ id: string | null;
18
+ parent_id: string | null;
19
+ user_id: string | null;
20
+ }
21
+
22
+ type GenericEntityAttributes = {
23
+ /**
24
+ * Entity groups
25
+ */
26
+ entity_id?: ANY_ENTITY[];
27
+ /**
28
+ * Human readable name
29
+ */
30
+ friendly_name?: string;
31
+ };
32
+
33
+ export type EntityHistoryItem = { a: object; s: unknown; lu: number };
34
+
35
+ export type TEntityUpdateCallback<ENTITY_ID extends ANY_ENTITY> = (
36
+ new_state: NonNullable<ENTITY_STATE<ENTITY_ID>>,
37
+ old_state: NonNullable<ENTITY_STATE<ENTITY_ID>>,
38
+ remove: () => TBlackHole,
39
+ ) => TBlackHole;
40
+
41
+ export type RemovableCallback<ENTITY_ID extends ANY_ENTITY> = (
42
+ callback: TEntityUpdateCallback<ENTITY_ID>,
43
+ ) => {
44
+ remove: () => void;
45
+ };
46
+
47
+ export type ByIdProxy<ENTITY_ID extends ANY_ENTITY> = ENTITY_STATE<ENTITY_ID> & {
48
+ entity_id: ENTITY_ID;
49
+ /**
50
+ * Run callback
51
+ */
52
+ onUpdate: RemovableCallback<ENTITY_ID>;
53
+ /**
54
+ * Retrieve state changes for an entity in a date range
55
+ */
56
+ history: (from: Dayjs | Date, to: Dayjs | Date) => Promise<ENTITY_STATE<ENTITY_ID>[]>;
57
+ /**
58
+ * Run callback once, for next update
59
+ */
60
+ once: (callback: TEntityUpdateCallback<ENTITY_ID>) => void;
61
+ /**
62
+ * Will resolve with the next state of the next value. No time limit
63
+ */
64
+ nextState: (timeoutMs?: number) => Promise<ENTITY_STATE<ENTITY_ID>>;
65
+ /**
66
+ * Will resolve when state
67
+ */
68
+ waitForState: (state: string | number, timeoutMs?: number) => Promise<ENTITY_STATE<ENTITY_ID>>;
69
+ /**
70
+ * Access the immediate previous entity state
71
+ */
72
+ previous: ENTITY_STATE<ENTITY_ID>;
73
+ /**
74
+ * Remove all `.onUpdate` listeners for this entity
75
+ *
76
+ * If you want to remove a particular one, use use the return value of the `.onUpdate` call instead
77
+ */
78
+ removeAllListeners: () => void;
79
+ } & (GetDomain<ENTITY_ID> extends ALL_SERVICE_DOMAINS
80
+ ? DomainServiceCalls<GetDomain<ENTITY_ID>>
81
+ : object);
82
+
83
+ type DomainServiceCalls<DOMAIN extends Extract<ALL_DOMAINS, ALL_SERVICE_DOMAINS>> = {
84
+ [SERVICE in Extract<keyof iCallService[DOMAIN], string>]: CallRewrite<DOMAIN, SERVICE>;
85
+ };
86
+
87
+ type CallRewrite<
88
+ D extends Extract<ALL_DOMAINS, ALL_SERVICE_DOMAINS>,
89
+ S extends keyof iCallService[D],
90
+ > = (
91
+ // @ts-expect-error fix another day, the transformation is valid
92
+ data?: Except<Parameters<iCallService[D][S]>[typeof FIRST], "entity_id">,
93
+ ) => Promise<void>;
94
+
95
+ export interface GenericEntityDTO<
96
+ ATTRIBUTES extends object = GenericEntityAttributes,
97
+ STATE extends unknown = string,
98
+ CONTEXT extends HassEntityContext = HassEntityContext,
99
+ DOMAIN extends TRawDomains = TRawDomains,
100
+ > {
101
+ attributes: ATTRIBUTES;
102
+ context: CONTEXT;
103
+ entity_id: PICK_ENTITY<DOMAIN>;
104
+ last_changed: string;
105
+ last_updated: string;
106
+ state: STATE;
107
+ }
108
+
109
+ export interface EventData<ID extends ANY_ENTITY = ANY_ENTITY> {
110
+ entity_id?: ID;
111
+ event?: number;
112
+ id?: string;
113
+ new_state?: ENTITY_STATE<ID>;
114
+ old_state?: ENTITY_STATE<ID>;
115
+ }
116
+ export type EntityUpdateEvent<
117
+ ID extends ANY_ENTITY = ANY_ENTITY,
118
+ CONTEXT extends HassEntityContext = HassEntityContext,
119
+ > = {
120
+ context: CONTEXT;
121
+ data: EventData<ID>;
122
+ event_type: string;
123
+ origin: "local";
124
+ result?: string;
125
+ time_fired: Date;
126
+ variables: Record<string, unknown>;
127
+ };
128
+
129
+ export interface EntityDetails<ENTITY extends ANY_ENTITY> {
130
+ area_id: TAreaId;
131
+ categories: Categories;
132
+ config_entry_id: null | string;
133
+ device_id: TDeviceId;
134
+ disabled_by: string | null;
135
+ entity_category: string | null;
136
+ entity_id: ENTITY;
137
+ has_entity_name: boolean;
138
+ hidden_by: string | null;
139
+ icon: null;
140
+ id: string;
141
+ labels: TLabelId[];
142
+ name: null | string;
143
+ options: Options;
144
+ original_name: null | string;
145
+ platform: TPlatformId;
146
+ translation_key: null | string;
147
+ unique_id: string;
148
+ }
149
+
150
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
151
+ export interface Categories {}
152
+
153
+ export interface Options {
154
+ conversation: Conversation;
155
+ "sensor.private"?: SensorPrivate;
156
+ sensor?: Sensor;
157
+ }
158
+
159
+ export interface Conversation {
160
+ should_expose: boolean;
161
+ }
162
+
163
+ export interface Sensor {
164
+ suggested_display_precision?: number;
165
+ display_precision?: null;
166
+ unit_of_measurement?: SensorUnitOfMeasurement;
167
+ }
168
+
169
+ export interface SensorPrivate {
170
+ suggested_unit_of_measurement: string;
171
+ }