@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,47 +1,57 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
|
|
3
|
+
import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
|
|
3
4
|
import type { IHomeAssistantTuyaConfig, TTuyaDatapointMapping } from './tuya.types.js';
|
|
4
5
|
import { tuyaDeviceId, tuyaDevices, tuyaEntities, tuyaEntityId, tuyaFeatureId, type ITuyaObservedDatapoint } from './tuya.mapper.js';
|
|
5
6
|
import { tuyaKeys, tuyaRecord, tuyaValueMatches } from './tuya.validation.js';
|
|
6
7
|
|
|
8
|
+
interface ITuyaCleanupResource {
|
|
9
|
+
readonly cleanup: () => Promise<void>;
|
|
10
|
+
promise?: Promise<void>;
|
|
11
|
+
failed: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export class TuyaRuntime implements IIntegrationRuntime {
|
|
8
15
|
public readonly domain = 'tuya';
|
|
9
16
|
readonly #config: IHomeAssistantTuyaConfig;
|
|
10
17
|
readonly #protocol: plugins.ecobridgeDeviceManager.TuyaProtocol;
|
|
11
|
-
readonly #
|
|
12
|
-
readonly #logger?: ILogger;
|
|
18
|
+
readonly #lease: INativeSetupLease;
|
|
13
19
|
readonly #observed = new Map<number, ITuyaObservedDatapoint>();
|
|
14
20
|
readonly #subscriptions = new Set<(eventArg: IIntegrationEvent) => void>();
|
|
15
21
|
readonly #subscriptionCleanup = new Set<() => void>();
|
|
16
|
-
readonly #
|
|
17
|
-
readonly #
|
|
22
|
+
readonly #resources: ITuyaCleanupResource[] = [];
|
|
23
|
+
readonly #callbacks = new Set<Promise<void>>();
|
|
24
|
+
#subscriberFailure = false;
|
|
18
25
|
#closed = false;
|
|
19
26
|
#destroy?: Promise<void>;
|
|
20
27
|
|
|
21
28
|
constructor(configArg: IHomeAssistantTuyaConfig, protocolArg: plugins.ecobridgeDeviceManager.TuyaProtocol,
|
|
22
|
-
reportsArg: readonly plugins.ecobridgeDeviceManager.ITuyaDatapointReport[],
|
|
29
|
+
reportsArg: readonly plugins.ecobridgeDeviceManager.ITuyaDatapointReport[], leaseArg: INativeSetupLease) {
|
|
23
30
|
this.#config = configArg;
|
|
24
31
|
this.#protocol = protocolArg;
|
|
25
|
-
this.#
|
|
26
|
-
this.#
|
|
32
|
+
this.#lease = leaseArg;
|
|
33
|
+
this.#resources.push({ cleanup: () => this.#protocol.close(), failed: false });
|
|
27
34
|
for (const report of reportsArg) this.#accept(report);
|
|
28
35
|
if (this.#observed.size === 0) throw new Error('Tuya device reported none of the configured product datapoints.');
|
|
29
|
-
|
|
36
|
+
const unsubscribe = this.#protocol.subscribe((report) => {
|
|
37
|
+
if (this.#lease.signal.aborted || this.#destroy) return;
|
|
30
38
|
try { this.#accept(report); }
|
|
31
39
|
catch {
|
|
32
40
|
this.#closed = true;
|
|
33
41
|
this.#emit({ type: 'error', data: { code: 'schema-mismatch', message: 'Tuya reported data no longer matches the configured product schema.' } });
|
|
34
42
|
this.#emit({ type: 'availability_changed', data: { available: false } });
|
|
35
|
-
void this.destroy();
|
|
43
|
+
void this.destroy().catch(() => undefined);
|
|
36
44
|
}
|
|
37
45
|
});
|
|
38
|
-
this.#
|
|
46
|
+
this.#resources.push({ cleanup: async () => unsubscribe(), failed: false });
|
|
47
|
+
const unsubscribeErrors = this.#protocol.subscribeErrors((error) => {
|
|
39
48
|
this.#emit({ type: 'error', data: { code: error.code, effectOutcome: error.effectOutcome } });
|
|
40
49
|
if (!this.#protocol.connected) this.#emit({ type: 'availability_changed', data: { available: false } });
|
|
41
50
|
});
|
|
51
|
+
this.#resources.push({ cleanup: async () => unsubscribeErrors(), failed: false });
|
|
42
52
|
}
|
|
43
53
|
|
|
44
|
-
public isHealthy(): boolean { return !this.#closed && this.#protocol.connected; }
|
|
54
|
+
public isHealthy(): boolean { return !this.#closed && !this.#lease.signal.aborted && this.#protocol.connected; }
|
|
45
55
|
|
|
46
56
|
public async devices(signalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
|
|
47
57
|
signalArg?.throwIfAborted();
|
|
@@ -53,6 +63,7 @@ export class TuyaRuntime implements IIntegrationRuntime {
|
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, signalArg?: AbortSignal): Promise<() => Promise<void>> {
|
|
66
|
+
this.#lease.assertOpen();
|
|
56
67
|
if (this.#closed) throw new Error('Tuya runtime is permanently closed.');
|
|
57
68
|
if (typeof handlerArg !== 'function') throw new TypeError('Tuya subscription handler must be a function.');
|
|
58
69
|
signalArg?.throwIfAborted();
|
|
@@ -106,16 +117,37 @@ export class TuyaRuntime implements IIntegrationRuntime {
|
|
|
106
117
|
} catch (errorArg) { return this.#failure(errorArg); }
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
public destroy(): Promise<void> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.#
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
public destroy(): Promise<void> { return this.#lease.close(); }
|
|
121
|
+
|
|
122
|
+
/** Invoked by the retained owner handle, including explicit cleanup-only retry. */
|
|
123
|
+
public closeOwnedResources(retryArg = false): Promise<void> {
|
|
124
|
+
if (retryArg) return this.#drain(true);
|
|
125
|
+
if (!this.#destroy) {
|
|
126
|
+
this.#closed = true;
|
|
127
|
+
this.#destroy = Promise.resolve().then(() => this.#drain(false));
|
|
128
|
+
}
|
|
116
129
|
return this.#destroy;
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
async #drain(retryArg: boolean): Promise<void> {
|
|
133
|
+
for (const cleanup of [...this.#subscriptionCleanup]) cleanup();
|
|
134
|
+
// The first close already joined every admitted callback. Explicit retry acknowledges its
|
|
135
|
+
// retained failure; subscriber code is never invoked again as part of cleanup.
|
|
136
|
+
if (retryArg) this.#subscriberFailure = false;
|
|
137
|
+
const cleanup = this.#resources.map((resourceArg) => {
|
|
138
|
+
if (retryArg && resourceArg.failed) { resourceArg.promise = undefined; resourceArg.failed = false; }
|
|
139
|
+
resourceArg.promise ??= Promise.resolve().then(resourceArg.cleanup).catch(() => {
|
|
140
|
+
resourceArg.failed = true;
|
|
141
|
+
throw new Error('Tuya owned resource cleanup failed.');
|
|
142
|
+
});
|
|
143
|
+
return resourceArg.promise;
|
|
144
|
+
});
|
|
145
|
+
const [results] = await Promise.all([Promise.allSettled(cleanup), Promise.allSettled([...this.#callbacks])]);
|
|
146
|
+
const failures = results.filter((resultArg): resultArg is PromiseRejectedResult => resultArg.status === 'rejected').map((resultArg) => resultArg.reason as unknown);
|
|
147
|
+
if (this.#subscriberFailure) failures.push(new Error('Tuya event subscriber failed.'));
|
|
148
|
+
if (failures.length) throw new AggregateError(failures, 'Tuya cleanup failed; ownership remains retained for explicit retry.');
|
|
149
|
+
}
|
|
150
|
+
|
|
119
151
|
#accept(reportArg: plugins.ecobridgeDeviceManager.ITuyaDatapointReport): void {
|
|
120
152
|
const changes: ITuyaObservedDatapoint[] = [];
|
|
121
153
|
for (const mapping of this.#config.datapoints) {
|
|
@@ -134,6 +166,7 @@ export class TuyaRuntime implements IIntegrationRuntime {
|
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
async #write(mappingArg: TTuyaDatapointMapping, valueArg: unknown, signalArg?: AbortSignal): Promise<IServiceCallResult> {
|
|
169
|
+
this.#lease.assertOpen();
|
|
137
170
|
if (!this.isHealthy()) throw new Error('Tuya session is unavailable; no automatic reconnect or retry is performed.');
|
|
138
171
|
if (mappingArg.kind === 'sensor' || mappingArg.access !== 'read-write') throw new Error('Tuya datapoint is read-only.');
|
|
139
172
|
if (!tuyaValueMatches(mappingArg, valueArg)) throw new TypeError('Tuya write value does not match its configured type/range/step.');
|
|
@@ -151,16 +184,29 @@ export class TuyaRuntime implements IIntegrationRuntime {
|
|
|
151
184
|
|
|
152
185
|
#emit(eventArg: Omit<IIntegrationEvent, 'integrationDomain' | 'deviceId' | 'timestamp'>): void {
|
|
153
186
|
for (const handler of [...this.#subscriptions]) {
|
|
154
|
-
if (this.#destroy) return;
|
|
187
|
+
if (this.#destroy || this.#lease.signal.aborted) return;
|
|
188
|
+
if (this.#callbacks.size >= 128) {
|
|
189
|
+
this.#subscriberFailed();
|
|
190
|
+
void this.destroy().catch(() => undefined);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Reserve a continuation before entering foreign code, including synchronous close reentry.
|
|
194
|
+
let finish!: () => void;
|
|
195
|
+
const continuation = new Promise<void>((resolveArg) => { finish = resolveArg; });
|
|
196
|
+
this.#callbacks.add(continuation);
|
|
197
|
+
const settled = (): void => { this.#callbacks.delete(continuation); finish(); };
|
|
155
198
|
try {
|
|
156
199
|
const result: unknown = handler({ ...structuredClone(eventArg), integrationDomain: 'tuya', deviceId: tuyaDeviceId(this.#config), timestamp: Date.now() });
|
|
157
|
-
if (result
|
|
158
|
-
|
|
200
|
+
if (result !== null && (typeof result === 'object' || typeof result === 'function')) {
|
|
201
|
+
void Promise.resolve(result).then(settled, () => { this.#subscriberFailed(); settled(); });
|
|
202
|
+
} else settled();
|
|
203
|
+
} catch { this.#subscriberFailed(); settled(); }
|
|
159
204
|
}
|
|
160
205
|
}
|
|
161
206
|
|
|
162
207
|
#subscriberFailed(): void {
|
|
163
|
-
|
|
164
|
-
this.#logger?.log('error', 'Tuya event subscriber failed.', { code: 'subscriber-error' });
|
|
208
|
+
this.#subscriberFailure = true;
|
|
209
|
+
try { this.#lease.context.logger?.log('error', 'Tuya event subscriber failed.', { code: 'subscriber-error' }); }
|
|
210
|
+
catch { /* Logging cannot detach or replace a retained subscriber failure. */ }
|
|
165
211
|
}
|
|
166
212
|
}
|
|
@@ -25,7 +25,7 @@ export const tuyaDevices = (configArg: IHomeAssistantTuyaConfig, observedArg: re
|
|
|
25
25
|
protocol: 'unknown', manufacturer: 'Tuya', online: availableArg,
|
|
26
26
|
features: observedArg.map(({ mapping }) => ({
|
|
27
27
|
id: tuyaFeatureId(mapping), name: mapping.name, readable: true,
|
|
28
|
-
writable: mapping.kind !== 'sensor' && mapping.access === 'read-write',
|
|
28
|
+
writable: availableArg && mapping.kind !== 'sensor' && mapping.access === 'read-write',
|
|
29
29
|
capability: mapping.kind === 'switch' ? 'switch' : mapping.kind === 'brightness' ? 'light' : 'sensor',
|
|
30
30
|
...(mapping.kind === 'sensor' && mapping.unit !== undefined ? { unit: mapping.unit } : {}),
|
|
31
31
|
})),
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
|
|
2
|
+
|
|
3
|
+
export interface ITuyaIntegrationOptions extends INativeIntegrationOwnerOptions {}
|
|
4
|
+
|
|
1
5
|
export type TTuyaDatapointAccess = 'read' | 'read-write';
|
|
2
6
|
|
|
3
7
|
export interface ITuyaDatapointIdentity {
|
|
@@ -97,15 +97,12 @@ export const normalizeTuyaConfig = (valueArg: unknown): IHomeAssistantTuyaConfig
|
|
|
97
97
|
});
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
/** Capture data fields before admission; the shared owner verifies home and actual store scope. */
|
|
101
|
+
export const tuyaSetupContext = (contextArg: IIntegrationSetupContext): IIntegrationSetupContext => {
|
|
101
102
|
const context = tuyaRecord(contextArg, 'setup context');
|
|
102
|
-
tuyaKeys(context, ['configStore', 'abortSignal', 'logger']);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const scope = store.getScope();
|
|
106
|
-
if (scope.homeId !== configArg.homeId || scope.integrationDomain !== 'tuya') throw new TypeError('Tuya encrypted store must belong to the exact configured home and integration.');
|
|
107
|
-
if (context.abortSignal !== undefined && !(context.abortSignal instanceof AbortSignal)) throw new TypeError('Tuya setup cancellation must be an AbortSignal.');
|
|
108
|
-
return store;
|
|
103
|
+
tuyaKeys(context, ['homeId', 'configStore', 'abortSignal', 'logger']);
|
|
104
|
+
if (!(context.configStore instanceof NoSqlConfigStore)) throw new TypeError('Tuya requires an encrypted NoSqlConfigStore and caller-owned encryption key provider.');
|
|
105
|
+
return Object.freeze(context) as IIntegrationSetupContext;
|
|
109
106
|
};
|
|
110
107
|
|
|
111
108
|
export const tuyaCredentialId = (deviceIdArg: string): string => `device:${deviceIdArg}`;
|
|
@@ -1,35 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LG webOS TV
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The native runtime pairs with a verified TV through its SSAP WebSocket endpoint,
|
|
4
|
+
stores pairing keys encrypted in the host's database, and observes power, volume,
|
|
5
|
+
mute, foreground application and external inputs through authoritative subscriptions.
|
|
6
|
+
It uses the installed `ws` transport and requires no Home Assistant server.
|
|
7
|
+
`HomeAssistantWebostvIntegration` remains an exported constructor alias.
|
|
4
8
|
|
|
5
9
|
```typescript
|
|
6
|
-
const integration = new WebostvIntegration({
|
|
10
|
+
const integration = new WebostvIntegration({
|
|
11
|
+
db,
|
|
12
|
+
keyProvider,
|
|
13
|
+
allowedCommands: ['volume_set', 'volume_mute'],
|
|
14
|
+
});
|
|
7
15
|
const runtime = await integration.setup({
|
|
8
|
-
homeId: 'home
|
|
9
|
-
endpoint: 'wss://
|
|
16
|
+
homeId: 'home:example',
|
|
17
|
+
endpoint: 'wss://tv.example:3001/',
|
|
10
18
|
expectedDeviceUUID: verifiedDeviceUUID,
|
|
11
19
|
pairingMode: 'prompt',
|
|
12
|
-
certificateAuthority:
|
|
13
|
-
}, {});
|
|
14
|
-
|
|
15
|
-
await runtime.
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
certificateAuthority: ownerCertificateChain,
|
|
21
|
+
}, { homeId: 'home:example', abortSignal });
|
|
22
|
+
|
|
23
|
+
const [device] = await runtime.devices();
|
|
24
|
+
const result = await runtime.callService!({
|
|
25
|
+
domain: 'webostv',
|
|
26
|
+
service: 'volume_set',
|
|
27
|
+
target: { deviceId: device.id },
|
|
28
|
+
data: { volume: 25 },
|
|
18
29
|
});
|
|
19
30
|
await integration.destroy();
|
|
20
31
|
```
|
|
21
32
|
|
|
22
|
-
The
|
|
33
|
+
The host independently selects `context.homeId`. The integration binds that home
|
|
34
|
+
once, including through config flow and failed setup. An optional constructor
|
|
35
|
+
`homeId` constrains the selected context. Configuration, discovery, form values
|
|
36
|
+
and database scope cannot select a home or grant control. Config flow captures
|
|
37
|
+
the selected home and rejects submitted home fields. Its forms stop accepting
|
|
38
|
+
submissions after flow close or integration destruction.
|
|
23
39
|
|
|
24
|
-
The
|
|
40
|
+
The exported client has the same host boundary:
|
|
41
|
+
`new WebostvClient(config, { homeId, abortSignal }, options)`. Construction performs
|
|
42
|
+
no pairing, provider calls or transport I/O. Setup requires either constructor
|
|
43
|
+
`db` and `keyProvider` together, or an actual `NoSqlConfigStore` in the context
|
|
44
|
+
with the exact selected home and `webostv` domain. Scope checks call the genuine
|
|
45
|
+
store implementation; overriding `getScope` cannot grant ownership. Context
|
|
46
|
+
stores, databases and key providers remain caller-owned.
|
|
47
|
+
The provider supplies 32-byte keys through `currentKey(signal)` and
|
|
48
|
+
`keyForId(id, signal)`. A correctly scoped context store takes precedence over
|
|
49
|
+
constructor persistence; an invalid supplied store fails without falling back.
|
|
25
50
|
|
|
26
|
-
|
|
51
|
+
Every effect defaults to denied. The host supplies a copied, immutable
|
|
52
|
+
`allowedCommands` list in constructor options. The supported exact values are
|
|
53
|
+
`turn_on`, `turn_off`, `volume_set`, `volume_up`, `volume_down`, `volume_mute`,
|
|
54
|
+
`select_source`, `media_play`, `media_pause` and `media_stop`. Unknown, duplicate,
|
|
55
|
+
sparse or accessor-bearing lists are rejected. Saved configuration and form
|
|
56
|
+
values cannot set permissions. Volume and mute features are writable only while
|
|
57
|
+
connected and explicitly authorized. Service targets and feature writes must
|
|
58
|
+
refer to this runtime's home-prefixed device and media-player entity; input
|
|
59
|
+
selection requires an advertised input ID. Deep-standby wake-on-LAN and
|
|
60
|
+
next/previous track controls are unsupported.
|
|
27
61
|
|
|
28
|
-
|
|
62
|
+
Services accept the `webostv` or `media_player` domain with these exact arguments:
|
|
29
63
|
|
|
30
|
-
| Service |
|
|
64
|
+
| Service | Data | SSAP endpoint |
|
|
31
65
|
| --- | --- | --- |
|
|
32
|
-
| `status`, `snapshot` | none | Last authoritative subscribed state
|
|
66
|
+
| `status`, `snapshot` | none | Last authoritative subscribed state and current availability |
|
|
33
67
|
| `volume_set` | `{ volume: integer 0..100 }` | `audio/setVolume` |
|
|
34
68
|
| `volume_up`, `volume_down` | none | `audio/volumeUp`, `audio/volumeDown` |
|
|
35
69
|
| `volume_mute` | `{ mute: boolean }` | `audio/setMute` |
|
|
@@ -37,12 +71,71 @@ Supported exact-target services in `webostv` or `media_player`:
|
|
|
37
71
|
| `media_play`, `media_pause`, `media_stop` | none | `media.controls/play`, `/pause`, `/stop` |
|
|
38
72
|
| `turn_on`, `turn_off` | none | `system/turnOn`, `system/turnOff` |
|
|
39
73
|
|
|
40
|
-
|
|
74
|
+
The TV must report the expected UUID before an existing pairing key is sent.
|
|
75
|
+
Stored keys are bound to both UUID and endpoint. `prompt` explicitly permits
|
|
76
|
+
pairing confirmation on the TV; `existing` refuses absent or rejected durable
|
|
77
|
+
pairing. TLS certificate validation stays enabled, and an owner-supplied PEM
|
|
78
|
+
chain can establish trust. Cleartext `ws` requires explicit
|
|
79
|
+
`allowUnencryptedConnection: true`. Credentials in endpoints, arbitrary URL
|
|
80
|
+
paths and fixture/injected-client production configs are rejected.
|
|
81
|
+
Discovery admits only explicit manual candidates with the domain and metadata
|
|
82
|
+
`{ endpoint, expectedDeviceUUID }`; it does not probe or verify a TV. Cleartext
|
|
83
|
+
UUID selection provides no cryptographic authentication. There is no automatic
|
|
84
|
+
WS downgrade, endpoint guessing, filesystem persistence or credential fallback.
|
|
85
|
+
The accepted key is stored before subscriptions start. Existing mode never
|
|
86
|
+
accepts fresh pairing, though the TV may display a prompt before disconnection.
|
|
87
|
+
|
|
88
|
+
A successful command receipt reports `acknowledgement: 'tv-response'` and
|
|
89
|
+
`physicalCompletion: 'unverified'`. Independent one-shot reads of the documented
|
|
90
|
+
state endpoints produce `observationStatus: 'observed'` and an observation when
|
|
91
|
+
available. Failure to read back an already accepted command produces
|
|
92
|
+
`observationStatus: 'unavailable'`; it never replays the command or substitutes
|
|
93
|
+
cached state as a fresh observation. Readback does not optimistically overwrite
|
|
94
|
+
subscription state. Model metadata comes from `system/getSystemInfo.modelName`
|
|
95
|
+
and stays absent when unreported. Owner permissions are not advertised as a
|
|
96
|
+
TV-reported per-command capability list.
|
|
97
|
+
Power-off timeout remains indeterminate even if the TV physically turned off.
|
|
98
|
+
`turn_on` requires an already connected TV supporting that method. Media controls
|
|
99
|
+
depend on the foreground application. All five state subscriptions and the
|
|
100
|
+
pre-registration system-info request are required; unsupported firmware fails
|
|
101
|
+
setup explicitly. Remote pointer sockets, app launching, channels and calibration
|
|
102
|
+
remain outside the runtime.
|
|
103
|
+
|
|
104
|
+
The owner defaults to a combined capacity of 16 pending operations, runtimes and
|
|
105
|
+
retained cleanup resources. Runtime destruction releases its setup lease.
|
|
106
|
+
Integration destruction is terminal and shares one close promise. The client
|
|
107
|
+
publishes its close promise before cancellation and availability callbacks, then
|
|
108
|
+
drains startup, commands, accepted async subscribers and owned store cleanup.
|
|
109
|
+
Unsubscribe removes admission and joins callbacks already accepted for that
|
|
110
|
+
subscription. Throwing or rejecting subscribers are isolated and removed.
|
|
111
|
+
Callbacks can request teardown; directly returning the client, runtime or owner's close promise
|
|
112
|
+
does not create a self-wait. A callback must not await teardown that is itself
|
|
113
|
+
waiting for that callback to finish.
|
|
114
|
+
|
|
115
|
+
Late encryption-provider cleanup failures reject close and remain retained.
|
|
116
|
+
After the provider settles, explicit `retryCleanup()` acknowledges cleanup
|
|
117
|
+
without replaying provider calls, pairing or commands, and without reopening
|
|
118
|
+
admission. A client closes only stores it created; borrowed stores remain usable.
|
|
119
|
+
Frames, pending requests, subscribers and inputs remain bounded. Heartbeat or
|
|
120
|
+
malformed authoritative state closes the instance. Disconnected snapshots retain
|
|
121
|
+
their observation timestamp and report unavailable. Reconnection requires a new
|
|
122
|
+
setup; there is no surviving command queue.
|
|
41
123
|
|
|
42
|
-
|
|
124
|
+
Migration requires an independently selected home in setup, flow and client
|
|
125
|
+
contexts, and explicit constructor command permissions for effects. The former
|
|
126
|
+
client `(config, persistenceOptions, signal)` signature becomes
|
|
127
|
+
`(config, { homeId, configStore?, abortSignal? }, options)`. Production `rawData`,
|
|
128
|
+
`snapshot`, `snapshotProvider`, injected clients/command executors and fixture
|
|
129
|
+
mapping remain unsupported; fixture data belongs only in tests.
|
|
43
130
|
|
|
44
|
-
|
|
131
|
+
Protocol and transport references:
|
|
45
132
|
|
|
46
|
-
|
|
133
|
+
- [SSAP endpoints in aiowebostv](https://github.com/home-assistant-libs/aiowebostv/blob/main/aiowebostv/endpoints.py)
|
|
134
|
+
- [ConnectSDK webOS service](https://github.com/ConnectSDK/Connect-SDK-Android-Core/blob/master/src/com/connectsdk/service/WebOSTVService.java)
|
|
135
|
+
- [ws transport documentation](https://github.com/websockets/ws/blob/master/doc/ws.md)
|
|
136
|
+
- [ws on npm](https://www.npmjs.com/package/ws)
|
|
47
137
|
|
|
48
|
-
|
|
138
|
+
Verification uses local WebSocket/TLS and disposable in-memory database fixtures.
|
|
139
|
+
Real pairing, firmware acceptance and owner-device control require separate owner
|
|
140
|
+
authorization. No TV-reported per-command service-list contract was confirmed;
|
|
141
|
+
the adapter does not invent one.
|