@smarthome.exchange/integrations 2.0.0 → 4.0.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/changelog.md +46 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/classes.nativeintegrationowner.d.ts +2 -0
- package/dist_ts/core/classes.nativeintegrationowner.js +5 -1
- package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
- package/dist_ts/core/classes.runtimemanager.js +11 -3
- package/dist_ts/core/types.d.ts +2 -0
- package/dist_ts/integrations/deconz/deconz.classes.client.d.ts +37 -29
- package/dist_ts/integrations/deconz/deconz.classes.client.js +521 -294
- package/dist_ts/integrations/deconz/deconz.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/deconz/deconz.classes.configflow.js +63 -33
- package/dist_ts/integrations/deconz/deconz.classes.integration.d.ts +4 -3
- package/dist_ts/integrations/deconz/deconz.classes.integration.js +85 -40
- package/dist_ts/integrations/deconz/deconz.mapper.d.ts +1 -0
- package/dist_ts/integrations/deconz/deconz.mapper.js +24 -6
- package/dist_ts/integrations/deconz/deconz.types.d.ts +43 -6
- package/dist_ts/integrations/deconz/deconz.validation.d.ts +7 -1
- package/dist_ts/integrations/deconz/deconz.validation.js +215 -4
- package/dist_ts/integrations/esphome/esphome.classes.client.d.ts +19 -7
- package/dist_ts/integrations/esphome/esphome.classes.client.js +183 -89
- package/dist_ts/integrations/esphome/esphome.classes.configflow.d.ts +5 -2
- package/dist_ts/integrations/esphome/esphome.classes.configflow.js +43 -33
- package/dist_ts/integrations/esphome/esphome.classes.integration.d.ts +8 -6
- package/dist_ts/integrations/esphome/esphome.classes.integration.js +71 -43
- package/dist_ts/integrations/esphome/esphome.discovery.js +4 -12
- package/dist_ts/integrations/esphome/esphome.mapper.d.ts +2 -2
- package/dist_ts/integrations/esphome/esphome.mapper.js +6 -4
- package/dist_ts/integrations/esphome/esphome.types.d.ts +31 -6
- package/dist_ts/integrations/esphome/esphome.types.js +1 -1
- package/dist_ts/integrations/esphome/esphome.validation.d.ts +7 -0
- package/dist_ts/integrations/esphome/esphome.validation.js +66 -0
- package/dist_ts/integrations/homeassistant/homeassistant.classes.client.d.ts +32 -4
- package/dist_ts/integrations/homeassistant/homeassistant.classes.client.js +345 -116
- package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.d.ts +8 -5
- package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.js +34 -8
- package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.d.ts +7 -4
- package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.js +44 -56
- package/dist_ts/integrations/homeassistant/homeassistant.types.d.ts +12 -1
- package/dist_ts/integrations/hue/hue.classes.client.d.ts +32 -4
- package/dist_ts/integrations/hue/hue.classes.client.js +331 -57
- package/dist_ts/integrations/hue/hue.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/hue/hue.classes.configflow.js +79 -27
- package/dist_ts/integrations/hue/hue.classes.integration.d.ts +3 -1
- package/dist_ts/integrations/hue/hue.classes.integration.js +83 -62
- package/dist_ts/integrations/hue/hue.mapper.d.ts +2 -0
- package/dist_ts/integrations/hue/hue.mapper.js +8 -6
- package/dist_ts/integrations/hue/hue.types.d.ts +32 -1
- package/dist_ts/integrations/hue/hue.validation.d.ts +9 -3
- package/dist_ts/integrations/hue/hue.validation.js +112 -11
- package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
- package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
- package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
- package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
- package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
- package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
- package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
- package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
- package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
- package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
- package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
- package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
- package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
- package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
- package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
- package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
- package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
- package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
- package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
- package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
- package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
- package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
- package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
- package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
- package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
- package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
- package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
- package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
- package/dist_ts/integrations/shelly/shelly.types.js +11 -2
- package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
- package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
- package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
- package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
- package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
- package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
- package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
- package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
- package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
- package/dist_ts/integrations/sonos/sonos.config.js +23 -2
- package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
- package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
- package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
- package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
- package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
- package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
- package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
- package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
- package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
- package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
- package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
- package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
- package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
- package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
- package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
- package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
- package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
- package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
- package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
- package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
- package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
- package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
- package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
- package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
- package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
- package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
- package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
- package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
- package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
- package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
- package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
- package/dist_ts/integrations/webostv/webostv.types.js +1 -1
- package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
- package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
- package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
- package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
- package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
- package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
- package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
- package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
- package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
- package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
- package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
- package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +10 -10
- package/readme.md +25 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/classes.nativeintegrationowner.ts +5 -0
- package/ts/core/classes.runtimemanager.ts +11 -2
- package/ts/core/types.ts +2 -0
- package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
- package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
- package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
- package/ts/integrations/deconz/deconz.mapper.ts +22 -5
- package/ts/integrations/deconz/deconz.types.ts +49 -6
- package/ts/integrations/deconz/deconz.validation.ts +177 -4
- package/ts/integrations/deconz/readme.md +15 -1
- package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
- package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
- package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
- package/ts/integrations/esphome/esphome.discovery.ts +3 -12
- package/ts/integrations/esphome/esphome.mapper.ts +9 -7
- package/ts/integrations/esphome/esphome.types.ts +35 -6
- package/ts/integrations/esphome/esphome.validation.ts +66 -0
- package/ts/integrations/esphome/readme.md +20 -6
- package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
- package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
- package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
- package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
- package/ts/integrations/homeassistant/readme.md +22 -6
- package/ts/integrations/hue/hue.classes.client.ts +288 -55
- package/ts/integrations/hue/hue.classes.configflow.ts +68 -25
- package/ts/integrations/hue/hue.classes.integration.ts +75 -54
- package/ts/integrations/hue/hue.mapper.ts +8 -5
- package/ts/integrations/hue/hue.types.ts +37 -1
- package/ts/integrations/hue/hue.validation.ts +102 -13
- package/ts/integrations/hue/readme.md +49 -12
- package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
- package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
- package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
- package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
- package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
- package/ts/integrations/mqtt/mqtt.types.ts +52 -0
- package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
- package/ts/integrations/mqtt/readme.md +12 -3
- package/ts/integrations/samsungtv/readme.md +11 -5
- package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
- package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
- package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
- package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
- package/ts/integrations/shelly/readme.md +22 -7
- package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
- package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
- package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
- package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
- package/ts/integrations/shelly/shelly.mapper.ts +19 -20
- package/ts/integrations/shelly/shelly.rpc.ts +2 -0
- package/ts/integrations/shelly/shelly.types.ts +50 -2
- package/ts/integrations/shelly/shelly.validation.ts +76 -0
- package/ts/integrations/sonos/readme.md +117 -38
- package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
- package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
- package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
- package/ts/integrations/sonos/sonos.config.ts +24 -2
- package/ts/integrations/sonos/sonos.mapper.ts +13 -8
- package/ts/integrations/sonos/sonos.matchers.ts +2 -2
- package/ts/integrations/sonos/sonos.types.ts +30 -1
- package/ts/integrations/tasmota/readme.md +16 -5
- package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
- package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
- package/ts/integrations/tasmota/tasmota.config.ts +17 -0
- package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
- package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
- package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
- package/ts/integrations/tasmota/tasmota.types.ts +43 -2
- package/ts/integrations/tuya/readme.md +52 -10
- package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
- package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
- package/ts/integrations/tuya/tuya.mapper.ts +1 -1
- package/ts/integrations/tuya/tuya.types.ts +4 -0
- package/ts/integrations/tuya/tuya.validation.ts +5 -8
- package/ts/integrations/webostv/readme.md +115 -22
- package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
- package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
- package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
- package/ts/integrations/webostv/webostv.mapper.ts +4 -4
- package/ts/integrations/webostv/webostv.types.ts +18 -3
- package/ts/integrations/webostv/webostv.validation.ts +22 -1
- package/ts/integrations/zigbee2mqtt/readme.md +22 -27
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
- package/ts/integrations/zwave_js/readme.md +42 -9
- package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
- package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
- package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
- package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
- package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
- package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
- package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
- package/ts/plugins.ts +2 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
|
|
3
|
+
import { normalizeHomeAssistantConfig } from './homeassistant.classes.client.js';
|
|
4
|
+
import { homeAssistantRecord } from './homeassistant.validation.js';
|
|
2
5
|
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
|
3
6
|
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
4
7
|
import type {
|
|
5
|
-
IIntegrationRuntime,
|
|
6
8
|
IIntegrationSetupContext,
|
|
7
9
|
} from '../../core/types.js';
|
|
8
10
|
import { HomeAssistantClient } from './homeassistant.classes.client.js';
|
|
9
11
|
import { HomeAssistantRuntime } from './homeassistant.classes.runtime.js';
|
|
10
12
|
import type {
|
|
11
13
|
IHomeAssistantHomeassistantConfig,
|
|
14
|
+
IHomeAssistantIntegrationOptions,
|
|
12
15
|
THomeAssistantWebSocketFactory,
|
|
13
16
|
} from './homeassistant.types.js';
|
|
14
17
|
|
|
@@ -29,26 +32,40 @@ export class HomeAssistantHomeassistantIntegration extends BaseIntegration<IHome
|
|
|
29
32
|
transport: 'websocket',
|
|
30
33
|
};
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
) {
|
|
35
|
+
private readonly owner: NativeIntegrationOwner;
|
|
36
|
+
private readonly webSocketFactory: THomeAssistantWebSocketFactory;
|
|
37
|
+
|
|
38
|
+
constructor(optionsOrFactoryArg: IHomeAssistantIntegrationOptions | THomeAssistantWebSocketFactory = {}) {
|
|
37
39
|
super();
|
|
40
|
+
const options = homeAssistantRecord(typeof optionsOrFactoryArg === 'function'
|
|
41
|
+
? { webSocketFactory: optionsOrFactoryArg } : optionsOrFactoryArg);
|
|
42
|
+
if (Object.keys(options).some((key) => !['homeId', 'capacity', 'webSocketFactory'].includes(key))) throw new TypeError('Home Assistant integration options contain unsupported fields.');
|
|
43
|
+
if (options.webSocketFactory !== undefined && typeof options.webSocketFactory !== 'function') throw new TypeError('Home Assistant webSocketFactory must be a function.');
|
|
44
|
+
this.webSocketFactory = options.webSocketFactory as THomeAssistantWebSocketFactory | undefined ?? ((urlArg) => new globalThis.WebSocket(urlArg));
|
|
45
|
+
this.owner = new NativeIntegrationOwner(this.domain, { homeId: options.homeId as string | undefined, capacity: options.capacity as number | undefined });
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
public async setup(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
public async setup(configArg: IHomeAssistantHomeassistantConfig, contextArg: IIntegrationSetupContext): Promise<HomeAssistantRuntime> {
|
|
49
|
+
// Validate/capture before deferred setup so later caller mutation cannot change scope.
|
|
50
|
+
normalizeHomeAssistantConfig(configArg);
|
|
51
|
+
const config = structuredClone(configArg);
|
|
52
|
+
return this.owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
|
|
53
|
+
const client = new HomeAssistantClient(config, leaseArg.context, plugins.homeAssistantWebsocket,
|
|
54
|
+
this.webSocketFactory, { owner: this.owner, lease: leaseArg });
|
|
55
|
+
let cleanupStarted = false;
|
|
56
|
+
leaseArg.own(() => {
|
|
57
|
+
const retry = cleanupStarted;
|
|
58
|
+
cleanupStarted = true;
|
|
59
|
+
return retry ? client.retryCleanup() : client.destroy();
|
|
60
|
+
});
|
|
61
|
+
await client.init(leaseArg.signal);
|
|
62
|
+
leaseArg.assertOpen();
|
|
63
|
+
const runtime = new HomeAssistantRuntime(client, leaseArg, this.owner);
|
|
64
|
+
leaseArg.promoteToRuntime();
|
|
65
|
+
return runtime;
|
|
66
|
+
});
|
|
51
67
|
}
|
|
52
68
|
|
|
53
|
-
public
|
|
69
|
+
public destroy(): Promise<void> { return this.owner.close(); }
|
|
70
|
+
public retryCleanup(): Promise<void> { return this.owner.retryCleanup(); }
|
|
54
71
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { INativeSetupLease, NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
|
|
1
2
|
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
|
|
2
3
|
import type {
|
|
3
4
|
IDeviceFeatureWriteRequest,
|
|
@@ -10,6 +11,7 @@ import { HomeAssistantMapper } from './homeassistant.mapper.js';
|
|
|
10
11
|
import { homeAssistantRecord } from './homeassistant.validation.js';
|
|
11
12
|
import type {
|
|
12
13
|
IHomeAssistantClient,
|
|
14
|
+
IHomeAssistantOperationSession,
|
|
13
15
|
IHomeAssistantTargetScope,
|
|
14
16
|
} from './homeassistant.types.js';
|
|
15
17
|
|
|
@@ -21,43 +23,44 @@ const abortError = (abortSignalArg: AbortSignal): Error => {
|
|
|
21
23
|
|
|
22
24
|
export class HomeAssistantRuntime implements IIntegrationRuntime {
|
|
23
25
|
public readonly domain = 'homeassistant';
|
|
24
|
-
private readonly subscriptions = new Set<() => void>();
|
|
25
26
|
private destroyed = false;
|
|
26
|
-
private cleanupComplete = false;
|
|
27
27
|
private destroyPromise?: Promise<void>;
|
|
28
28
|
private readonly targetScope: IHomeAssistantTargetScope;
|
|
29
29
|
|
|
30
|
-
constructor(private readonly client: IHomeAssistantClient) {
|
|
30
|
+
constructor(private readonly client: IHomeAssistantClient, private readonly lease?: INativeSetupLease, private readonly owner?: NativeIntegrationOwner) {
|
|
31
31
|
this.targetScope = Object.freeze({ ...client.getTargetScope() });
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
public isHealthy(): boolean {
|
|
35
|
-
return !this.destroyed && this.client.connected;
|
|
35
|
+
return !this.destroyed && !this.lease?.signal.aborted && this.client.connected;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
public async devices(
|
|
39
39
|
abortSignalArg?: AbortSignal
|
|
40
40
|
): Promise<shxInterfaces.data.IDeviceDefinition[]> {
|
|
41
41
|
this.assertActive();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
42
|
+
return this.client.withOperation(async () => {
|
|
43
|
+
this.assertActive();
|
|
44
|
+
return HomeAssistantMapper.toDevices(
|
|
45
|
+
this.client.getSnapshot(),
|
|
46
|
+
this.client.connected,
|
|
47
|
+
this.client.getEntityPlatforms(),
|
|
48
|
+
this.client.getServices(),
|
|
49
|
+
this.targetScope
|
|
50
|
+
);
|
|
51
|
+
}, abortSignalArg);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
public async entities(): Promise<IIntegrationEntity[]> {
|
|
55
55
|
this.assertActive();
|
|
56
|
-
return
|
|
56
|
+
return this.client.withOperation(async () => {
|
|
57
|
+
this.assertActive();
|
|
58
|
+
return HomeAssistantMapper.toEntities(this.client.getSnapshot(), this.client.connected, this.targetScope);
|
|
59
|
+
});
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
public async subscribe(
|
|
60
|
-
handlerArg: (eventArg: IIntegrationEvent) => void
|
|
63
|
+
handlerArg: (eventArg: IIntegrationEvent) => void | Promise<void>,
|
|
61
64
|
abortSignalArg?: AbortSignal
|
|
62
65
|
): Promise<() => Promise<void>> {
|
|
63
66
|
this.assertActive();
|
|
@@ -65,32 +68,14 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
|
|
|
65
68
|
throw abortError(abortSignalArg);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
handlerArg({
|
|
71
|
+
if (typeof handlerArg !== 'function') throw new TypeError('Home Assistant subscriber must be a function.');
|
|
72
|
+
return this.client.subscribe(() => {
|
|
73
|
+
return handlerArg({
|
|
71
74
|
type: 'state_changed',
|
|
72
75
|
integrationDomain: this.domain,
|
|
73
76
|
timestamp: Date.now(),
|
|
74
77
|
});
|
|
75
|
-
});
|
|
76
|
-
const unsubscribe = (): void => {
|
|
77
|
-
if (!subscribed) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
subscribed = false;
|
|
81
|
-
this.subscriptions.delete(unsubscribe);
|
|
82
|
-
abortSignalArg?.removeEventListener('abort', unsubscribe);
|
|
83
|
-
unsubscribeClient();
|
|
84
|
-
};
|
|
85
|
-
this.subscriptions.add(unsubscribe);
|
|
86
|
-
abortSignalArg?.addEventListener('abort', unsubscribe, { once: true });
|
|
87
|
-
if (abortSignalArg?.aborted) {
|
|
88
|
-
unsubscribe();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return async () => {
|
|
92
|
-
unsubscribe();
|
|
93
|
-
};
|
|
78
|
+
}, abortSignalArg);
|
|
94
79
|
}
|
|
95
80
|
|
|
96
81
|
public async writeFeature(
|
|
@@ -101,6 +86,19 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
|
|
|
101
86
|
if (abortSignalArg?.aborted) {
|
|
102
87
|
throw abortError(abortSignalArg);
|
|
103
88
|
}
|
|
89
|
+
homeAssistantRecord(requestArg);
|
|
90
|
+
homeAssistantRecord(requestArg.device);
|
|
91
|
+
homeAssistantRecord(requestArg.feature);
|
|
92
|
+
if (requestArg.device.metadata !== undefined) homeAssistantRecord(requestArg.device.metadata);
|
|
93
|
+
const request = structuredClone(requestArg);
|
|
94
|
+
return this.client.withOperation((sessionArg) => this.writeAdmitted(request, sessionArg), abortSignalArg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async writeAdmitted(
|
|
98
|
+
requestArg: IDeviceFeatureWriteRequest,
|
|
99
|
+
sessionArg: IHomeAssistantOperationSession
|
|
100
|
+
): Promise<IServiceCallResult> {
|
|
101
|
+
this.assertActive();
|
|
104
102
|
if (Object.keys(homeAssistantRecord(requestArg)).some((key) => !['device', 'feature', 'value'].includes(key))) {
|
|
105
103
|
return { success: false, error: 'Home Assistant feature requests contain unsupported fields.' };
|
|
106
104
|
}
|
|
@@ -145,10 +143,8 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
|
|
|
145
143
|
return { success: false, error: mappedCall.error || 'Unsupported Home Assistant write.' };
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
const data = await
|
|
149
|
-
|
|
150
|
-
throw abortError(abortSignalArg);
|
|
151
|
-
}
|
|
146
|
+
const data = await sessionArg.callService(mappedCall.request);
|
|
147
|
+
sessionArg.signal.throwIfAborted();
|
|
152
148
|
return {
|
|
153
149
|
success: true,
|
|
154
150
|
data: {
|
|
@@ -159,34 +155,25 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
|
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
private assertActive(): void {
|
|
158
|
+
this.lease?.assertOpen();
|
|
162
159
|
if (this.destroyed) throw new Error('Home Assistant runtime is destroyed.');
|
|
163
160
|
}
|
|
164
161
|
|
|
165
162
|
public destroy(): Promise<void> {
|
|
166
|
-
if (this.destroyPromise) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (this.cleanupComplete) {
|
|
170
|
-
return Promise.resolve();
|
|
163
|
+
if (!this.destroyPromise) {
|
|
164
|
+
this.destroyed = true;
|
|
165
|
+
this.destroyPromise = Promise.resolve().then(() => this.performDestroy());
|
|
171
166
|
}
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (this.destroyPromise === destroyPromise) {
|
|
179
|
-
this.destroyPromise = undefined;
|
|
180
|
-
}
|
|
181
|
-
}).catch(() => undefined);
|
|
182
|
-
return destroyPromise;
|
|
167
|
+
return this.destroyPromise;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public retryCleanup(): Promise<void> {
|
|
171
|
+
if (!this.destroyPromise) return Promise.reject(new Error('Home Assistant cleanup retry requires destruction.'));
|
|
172
|
+
return this.owner ? this.owner.retryCleanup() : this.client.retryCleanup();
|
|
183
173
|
}
|
|
184
174
|
|
|
185
175
|
private async performDestroy(): Promise<void> {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
this.subscriptions.clear();
|
|
190
|
-
await this.client.destroy();
|
|
176
|
+
// The client closes transport before joining its owned subscriber continuations.
|
|
177
|
+
await (this.lease ? this.lease.close() : this.client.destroy());
|
|
191
178
|
}
|
|
192
179
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
|
|
1
2
|
import type * as homeAssistantWebsocket from 'home-assistant-js-websocket';
|
|
2
3
|
|
|
3
4
|
export type THomeAssistantVacuumCommand =
|
|
@@ -8,6 +9,10 @@ export type THomeAssistantVacuumCommand =
|
|
|
8
9
|
| 'locate'
|
|
9
10
|
| 'clean_spot';
|
|
10
11
|
|
|
12
|
+
export interface IHomeAssistantIntegrationOptions extends INativeIntegrationOwnerOptions {
|
|
13
|
+
readonly webSocketFactory?: THomeAssistantWebSocketFactory;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export interface IHomeAssistantHomeassistantConfig {
|
|
12
17
|
homeId: string;
|
|
13
18
|
baseUrl: string;
|
|
@@ -72,12 +77,23 @@ export interface IHomeAssistantClient {
|
|
|
72
77
|
getEntityPlatforms(): Record<string, string>;
|
|
73
78
|
getServices(): ReadonlySet<string>;
|
|
74
79
|
getTargetScope(): IHomeAssistantTargetScope;
|
|
75
|
-
subscribe(handlerArg: () => void): () => void
|
|
80
|
+
subscribe(handlerArg: () => void | Promise<void>, abortSignalArg?: AbortSignal): () => Promise<void>;
|
|
81
|
+
withOperation<TResult>(
|
|
82
|
+
workArg: (sessionArg: IHomeAssistantOperationSession) => Promise<TResult>,
|
|
83
|
+
abortSignalArg?: AbortSignal
|
|
84
|
+
): Promise<TResult>;
|
|
76
85
|
callService(
|
|
77
86
|
requestArg: IHomeAssistantServiceCall,
|
|
78
87
|
abortSignalArg?: AbortSignal
|
|
79
88
|
): Promise<unknown>;
|
|
80
89
|
destroy(): Promise<void>;
|
|
90
|
+
retryCleanup(): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A service session remains usable only inside its admitted continuation. */
|
|
94
|
+
export interface IHomeAssistantOperationSession {
|
|
95
|
+
readonly signal: AbortSignal;
|
|
96
|
+
callService(requestArg: IHomeAssistantServiceCall): Promise<unknown>;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
export interface IHomeAssistantTargetScope {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Home Assistant runtime
|
|
2
2
|
|
|
3
|
-
`HomeAssistantHomeassistantIntegration` connects directly to Home Assistant's authenticated `/api/websocket` endpoint through the installed official `home-assistant-js-websocket` client and Node's native WebSocket. Its existing
|
|
3
|
+
`HomeAssistantHomeassistantIntegration` connects directly to Home Assistant's authenticated `/api/websocket` endpoint through the installed official `home-assistant-js-websocket` client and Node's native WebSocket. Its existing WebSocket-factory constructor remains available; an options object additionally accepts `homeId`, `capacity` and `webSocketFactory`. Production contains no fixture entities or credentials.
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
|
-
const integration = new HomeAssistantHomeassistantIntegration();
|
|
6
|
+
const integration = new HomeAssistantHomeassistantIntegration({ capacity: 16 });
|
|
7
7
|
const runtime = await integration.setup({
|
|
8
8
|
homeId: 'house-a',
|
|
9
9
|
baseUrl: 'https://ha.example.test:8123',
|
|
@@ -14,15 +14,19 @@ const runtime = await integration.setup({
|
|
|
14
14
|
setupRetry: 0,
|
|
15
15
|
initialSnapshotTimeoutMs: 10_000,
|
|
16
16
|
serviceCallTimeoutMs: 15_000,
|
|
17
|
-
}, { abortSignal });
|
|
17
|
+
}, { homeId: 'house-a', abortSignal });
|
|
18
18
|
try {
|
|
19
19
|
const devices = await runtime.devices();
|
|
20
20
|
// Send selected advertised features through the hub's scoped tool boundary.
|
|
21
21
|
} finally {
|
|
22
|
-
await
|
|
22
|
+
await integration.destroy();
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Setup requires an independent host-selected `context.homeId`, even if the constructor constrains a home. Configuration home is an exact consistency check. The first admitted home remains fixed after failed readiness; context is frozen before allocation. An optional borrowed `NoSqlConfigStore` must be a real instance with exactly that home and the `homeassistant` domain; HA never closes it, its database, logger or caller-owned signal.
|
|
27
|
+
|
|
28
|
+
For a host-owned encrypted config store, pass `{ homeId: selectedHome, configStore: homeAssistantStore, abortSignal }` as context. The store's scope must be `{ homeId: selectedHome, integrationDomain: 'homeassistant' }`. The host closes that store after its integrations finish. Supplying a store does not replace the required inline runtime token or create a credential-reference API.
|
|
29
|
+
|
|
26
30
|
`homeId`, an HTTP(S) origin without user information, path, query or fragment, and a nonempty configured access token are required. Home IDs contain 1–128 alphanumeric, underscore, period, colon or hyphen characters and begin with an alphanumeric character. The exact configured ID is preserved, including case and colons in IDs such as `home:studio` and `home:unconfigured`; it is never trimmed or renamed. Optional entity IDs and domains form intersecting allowlists. Credentials stay in runtime configuration; a caller that persists configuration must use the core encrypted NoSql config store and its external key provider. This integration writes no application data to the filesystem.
|
|
27
31
|
|
|
28
32
|
Setup uses one deadline for authentication, subscription acknowledgements, an authoritative `get_states` snapshot, `get_services`, and `config/entity_registry/list_for_display`. Registry provenance is fetched for selected entities even when strict registry enforcement is disabled. Strict enforcement additionally requires every explicitly configured entity to exist in the state and registry snapshots. Home Assistant must support these documented commands; an unsupported or malformed response rejects setup.
|
|
@@ -33,8 +37,20 @@ Supported writes are exact light/switch/input-boolean `turn_on` and `turn_off`,
|
|
|
33
37
|
|
|
34
38
|
`state_changed` updates are authoritative. Service acknowledgements never synthesize state. `service_registered`, `service_removed` and `entity_registry_updated` events revoke snapshot readiness until fresh state, service and registry snapshots complete. Reconnects do the same before health or writes recover. Missing required entities, offline entities, malformed states or inconsistent event targets block controls. A malformed state event closes its connection and leaves retained devices offline.
|
|
35
39
|
|
|
36
|
-
Snapshots and buffered events are bounded at 10,000 entities, services at 10,000 actions and 1,000 domains, and entity attributes at depth 16 and 4,096 values. Caller aborts and write deadlines cancel the pending command through the owning connection; commands are never retried or replayed by this adapter. Cancellation after dispatch leaves the physical effect uncertain. Destroy aborts pending callers, releases all subscriptions, closes sockets, waits for physical closure up to one second, clears retained data and makes the runtime terminal.
|
|
40
|
+
Snapshots and buffered events are bounded at 10,000 entities, services at 10,000 actions and 1,000 domains, and entity attributes at depth 16 and 4,096 values. Caller aborts and write deadlines cancel the pending command through the owning connection; commands are never retried or replayed by this adapter. Cancellation after dispatch leaves the physical effect uncertain. Destroy aborts pending callers, releases all subscriptions, closes sockets, waits for physical closure up to one second, clears retained data and makes the runtime terminal. Repeated `destroy()` calls return the same permanent result, including a failure. `retryCleanup()` retries only failed cleanup handles and never reopens, authenticates or replays a command. Integration `destroy()` drains setup and all promoted runtimes, returns one permanent result and rejects reopening; integration `retryCleanup()` performs cleanup only. Runtime destruction closes its setup lease. Cancellation of the setup context remains attached after promotion. A shared capacity (default 16) counts pending work, live runtimes and retained cleanup; rejection happens before allocation. The exported controlling client also requires independent context: `new HomeAssistantClient(config, { homeId: selectedHome }, api?, webSocketFactory?)`.
|
|
41
|
+
|
|
42
|
+
Close joins actual connection, subscription, refresh and service settlement even when caller cancellation has already rejected. A late handle remains owned until its cleanup completes. Sibling cleanup proceeds even when one unsubscribe fails.
|
|
43
|
+
|
|
44
|
+
Runtime reads, feature writes, direct service calls and subscriber delivery use the same owner capacity as live runtimes. `client.withOperation(async (session) => ...)` owns the entire continuation and provides `session.signal` and a scoped `session.callService(request)`; a session cannot dispatch after that continuation finishes. Caller cancellation rejects promptly while the actual continuation and any dispatched service promise still occupy capacity until settlement. Requests are captured before deferred admission and validated against current authoritative state at dispatch.
|
|
45
|
+
|
|
46
|
+
Subscribers may return `Promise<void>`. The returned unsubscribe function fences new and queued delivery immediately, returns one stable promise and joins callbacks already running. Runtime and integration close shut down transport before joining callbacks. Synchronous and asynchronous callback failures, and rejected callback admission, remain observable through unsubscribe and close; a failed subscriber stops receiving events while siblings continue. Explicit cleanup retry clears reported callback failures and retries retained cleanup without replaying callbacks or commands. A callback must finish its own work before awaiting destruction of its owning runtime.
|
|
47
|
+
|
|
48
|
+
The local test fixtures use real HTTP upgrades and native WebSockets with test-only tokens. Their delayed replies preserve the result captured when a command was received. They verify exact request bytes, acknowledgement gates, catalogue changes, registry refresh, reconnect readiness, authoritative state, malformed inputs, cross-home and mutated-target rejection, shared capacity, callback drainage, cancellation and physical closure. The five suites contain 62 tests: injected behavior 20, native protocol 15, lifecycle 6, native ownership 9, finite operations/subscribers 12. The domain has seven production TypeScript roots and seven test/support roots for strict type and emit checks.
|
|
49
|
+
|
|
50
|
+
Qualification commands are `pnpm exec tstest test/homeassistant/ --verbose --logfile --timeout 60`, `pnpm run build`, and `pnpm exec tsbuild check 'test/**/*'` without `skipLibCheck`. For the domain's 14 roots, use the installed `@git.zone/tsbuild` `TsCompiler.checkTypes(roots, { strict: true })` and `checkEmit(roots, { strict: true })`; the package build also covers the shared owner and generated domains.
|
|
51
|
+
|
|
52
|
+
One upstream contract remains blocked. In installed `home-assistant-js-websocket` 9.7.0, terminal `Connection.close()` leaves an unacknowledged `subscribeEvents()` promise pending forever. `Connection._handleClose` preserves subscription commands for reconnect before checking `closeRequested`, then stops reconnecting. A fixture probe sent one subscription, closed its socket and observed no subscription settlement. The published 9.7.1 artifact was inspected on 2026-10-11 and retains the same implementation. HA's actual-work ledger deliberately continues owning that promise; shutdown during an unacknowledged subscription cannot be qualified as complete. The proper path is a fix and published release in the official client that rejects unresolved subscriptions on terminal close, followed by a published consumer update and native terminal-subscription regressions. No local shim, command replay or fabricated settlement is used.
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
Real Home Assistant authentication, registry compatibility, credential revocation, commissioning/control and physical shutdown acceptance remain owner-gated. Credential references and config flow, explicit control/plaintext consent, public offline-mapper defaults and `reconnect-error` reporting remain separate public-contract decisions.
|
|
39
55
|
|
|
40
56
|
API references: [Home Assistant WebSocket API](https://developers.home-assistant.io/docs/api/websocket/), [REST API](https://developers.home-assistant.io/docs/api/rest/), and [official JavaScript client](https://github.com/home-assistant/home-assistant-js-websocket).
|