@smarthome.exchange/integrations 2.0.0 → 3.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 +35 -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 +6 -1
- package/dist_ts/integrations/hue/hue.classes.client.js +120 -45
- package/dist_ts/integrations/hue/hue.classes.configflow.js +16 -7
- package/dist_ts/integrations/hue/hue.classes.integration.d.ts +2 -0
- package/dist_ts/integrations/hue/hue.classes.integration.js +103 -30
- package/dist_ts/integrations/hue/hue.validation.d.ts +1 -0
- package/dist_ts/integrations/hue/hue.validation.js +14 -4
- 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 +8 -8
- 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 +110 -41
- package/ts/integrations/hue/hue.classes.configflow.ts +12 -6
- package/ts/integrations/hue/hue.classes.integration.ts +92 -26
- package/ts/integrations/hue/hue.validation.ts +13 -3
- package/ts/integrations/hue/readme.md +3 -3
- 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,7 +1,10 @@
|
|
|
1
1
|
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
2
|
+
import * as plugins from '../../plugins.js';
|
|
3
|
+
import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
|
|
2
4
|
import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
|
|
3
5
|
import { normalizeTasmotaConfig } from './tasmota.config.js';
|
|
4
|
-
import type { IHomeAssistantTasmotaConfig } from './tasmota.types.js';
|
|
6
|
+
import type { IHomeAssistantTasmotaConfig, ITasmotaIntegrationOptions } from './tasmota.types.js';
|
|
7
|
+
import { createTasmotaCredential, tasmotaBrokerIdentity, tasmotaCopy, tasmotaCredentialKey, tasmotaData, tasmotaOptions, tasmotaText } from './tasmota.credentials.js';
|
|
5
8
|
|
|
6
9
|
class TasmotaManualMatcher implements IDiscoveryMatcher<IDiscoveryCandidate> {
|
|
7
10
|
public readonly id = 'tasmota-selected-device';
|
|
@@ -35,8 +38,28 @@ export const createTasmotaDiscoveryDescriptor = (): DiscoveryDescriptor => {
|
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfig> {
|
|
41
|
+
private readonly scope: NativeIntegrationScope;
|
|
42
|
+
private readonly hasResolver: boolean;
|
|
43
|
+
|
|
44
|
+
constructor(optionsArg: ITasmotaIntegrationOptions = {}, ownerArg?: NativeIntegrationOwner) {
|
|
45
|
+
const options = tasmotaOptions(optionsArg);
|
|
46
|
+
const owner = ownerArg ?? new NativeIntegrationOwner('tasmota', { homeId: options.homeId, capacity: options.capacity });
|
|
47
|
+
this.scope = owner.createFlowScope();
|
|
48
|
+
this.hasResolver = options.credentialResolver !== undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public destroy(): Promise<void> { return this.scope.close(); }
|
|
52
|
+
public retryCleanup(): Promise<void> { return this.scope.retryCleanup(); }
|
|
53
|
+
|
|
38
54
|
public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IHomeAssistantTasmotaConfig>> {
|
|
39
|
-
|
|
55
|
+
const candidate = tasmotaData(tasmotaCopy(candidateArg), 'Tasmota candidate');
|
|
56
|
+
const metadata = candidate.metadata === undefined ? undefined : tasmotaData(candidate.metadata, 'Tasmota candidate metadata');
|
|
57
|
+
const context = this.scope.admit(contextArg);
|
|
58
|
+
const candidateHost = candidate.host;
|
|
59
|
+
const candidatePort = candidate.port;
|
|
60
|
+
const candidateTopic = metadata?.topic;
|
|
61
|
+
let pending = false;
|
|
62
|
+
let submitted = false;
|
|
40
63
|
return {
|
|
41
64
|
kind: 'form',
|
|
42
65
|
title: 'Connect a selected Tasmota device',
|
|
@@ -45,6 +68,8 @@ export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfi
|
|
|
45
68
|
{ name: 'host', label: 'MQTT broker host', type: 'text', required: true },
|
|
46
69
|
{ name: 'port', label: 'MQTT broker port', type: 'number' },
|
|
47
70
|
{ name: 'useTls', label: 'Use MQTT TLS', type: 'boolean' },
|
|
71
|
+
{ name: 'anonymous', label: 'Use an anonymous broker', type: 'boolean' },
|
|
72
|
+
{ name: 'credentialRef', label: 'Existing broker credential reference', type: 'text' },
|
|
48
73
|
{ name: 'username', label: 'Broker username', type: 'text' },
|
|
49
74
|
{ name: 'password', label: 'Broker password', type: 'password' },
|
|
50
75
|
{ name: 'topic', label: 'Device Topic', type: 'text', required: true },
|
|
@@ -53,31 +78,69 @@ export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfi
|
|
|
53
78
|
{ name: 'brightness', label: 'Single-channel Dimmer', type: 'boolean' },
|
|
54
79
|
],
|
|
55
80
|
submit: async (valuesArg) => {
|
|
81
|
+
if (pending || submitted) return { kind: 'error', error: 'This Tasmota form is already submitting or completed.' };
|
|
82
|
+
pending = true;
|
|
56
83
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
const values = tasmotaData(tasmotaCopy(valuesArg), 'Tasmota form');
|
|
85
|
+
return await this.scope.run(context, context.homeId, async (leaseArg) => {
|
|
86
|
+
if (Object.keys(values).some((keyArg) => !['homeId', 'host', 'port', 'useTls', 'anonymous', 'credentialRef', 'username', 'password', 'topic', 'fullTopic', 'relayCount', 'brightness'].includes(keyArg))) throw new Error('Tasmota form contains unsupported fields.');
|
|
87
|
+
if (values.homeId !== undefined && values.homeId !== context.homeId) throw new Error('Tasmota form home conflicts with the admitted host home.');
|
|
88
|
+
for (const field of ['username', 'password', 'fullTopic', 'credentialRef']) if (values[field] !== undefined && typeof values[field] !== 'string') throw new Error(`${field} must be a string.`);
|
|
89
|
+
for (const field of ['useTls', 'brightness', 'anonymous']) if (values[field] !== undefined && typeof values[field] !== 'boolean') throw new Error(`${field} must be boolean.`);
|
|
90
|
+
const relayCount = values.relayCount ?? 1;
|
|
91
|
+
if (typeof relayCount !== 'number' || !Number.isInteger(relayCount) || relayCount < 0 || relayCount > 32) throw new Error('relayCount must be an integer from 0 through 32.');
|
|
92
|
+
const host = values.host ?? candidateHost;
|
|
93
|
+
const topic = values.topic ?? candidateTopic;
|
|
94
|
+
if (typeof host !== 'string' || typeof topic !== 'string') throw new Error('Broker host and device topic are required.');
|
|
95
|
+
const port = values.port ?? candidatePort;
|
|
96
|
+
if (port !== undefined && (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535)) throw new Error('Broker port must be an integer from 1 through 65535.');
|
|
97
|
+
const secretInput = values.username !== undefined || values.password !== undefined;
|
|
98
|
+
const anonymous = values.anonymous === true;
|
|
99
|
+
if (anonymous && (secretInput || values.credentialRef !== undefined)) throw new Error('Anonymous Tasmota mode cannot contain credentials.');
|
|
100
|
+
if (!anonymous && !secretInput && values.credentialRef === undefined) throw new Error('Select anonymous mode explicitly or provide broker credentials.');
|
|
101
|
+
if (secretInput && values.credentialRef !== undefined) throw new Error('Provide transient credentials or an existing reference, not both.');
|
|
102
|
+
const reference = anonymous ? undefined : secretInput ? plugins.crypto.randomUUID() : tasmotaText(values.credentialRef, 'Credential reference', 512);
|
|
103
|
+
const config: IHomeAssistantTasmotaConfig = {
|
|
104
|
+
homeId: context.homeId,
|
|
105
|
+
authentication: reference === undefined ? { mode: 'anonymous' } : { mode: 'credential-reference', credentialRef: reference },
|
|
106
|
+
broker: {
|
|
107
|
+
host,
|
|
108
|
+
port,
|
|
109
|
+
protocol: values.useTls === true ? 'mqtts' : 'mqtt',
|
|
110
|
+
},
|
|
111
|
+
devices: [{ topic, fullTopic: typeof values.fullTopic === 'string' && values.fullTopic ? values.fullTopic : undefined, relays: Array.from({ length: relayCount }, (_, indexArg) => indexArg + 1), brightness: values.brightness === true }],
|
|
112
|
+
};
|
|
113
|
+
const normalized = normalizeTasmotaConfig(config);
|
|
114
|
+
if (!anonymous) {
|
|
115
|
+
const store = leaseArg.context.configStore;
|
|
116
|
+
if (secretInput) {
|
|
117
|
+
if (!store) throw new Error('Transient Tasmota broker credentials require an admitted encrypted config store.');
|
|
118
|
+
const credential = createTasmotaCredential(tasmotaBrokerIdentity(normalized),
|
|
119
|
+
typeof values.username === 'string' ? values.username : undefined,
|
|
120
|
+
typeof values.password === 'string' ? values.password : undefined);
|
|
121
|
+
const key = tasmotaCredentialKey(reference!);
|
|
122
|
+
let committed = false;
|
|
123
|
+
let write: Promise<void> = Promise.resolve();
|
|
124
|
+
leaseArg.own(async () => {
|
|
125
|
+
const result = await Promise.allSettled([write]);
|
|
126
|
+
if (result[0].status === 'fulfilled' && !committed) {
|
|
127
|
+
try { await store.delete(key); }
|
|
128
|
+
catch { throw new Error('Tasmota cancelled credential cleanup failed.'); }
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
write = Promise.resolve().then(() => store.create(key, credential));
|
|
132
|
+
try { await write; }
|
|
133
|
+
catch { throw new Error('Tasmota encrypted credential persistence failed.'); }
|
|
134
|
+
leaseArg.assertOpen();
|
|
135
|
+
committed = true;
|
|
136
|
+
} else if (!store && !this.hasResolver) throw new Error('Tasmota credential reference requires an explicit credential source.');
|
|
137
|
+
}
|
|
138
|
+
submitted = true;
|
|
139
|
+
return { kind: 'done', title: 'Selected Tasmota device configured', config };
|
|
140
|
+
});
|
|
78
141
|
} catch (errorArg) {
|
|
79
142
|
return { kind: 'error', error: errorArg instanceof Error ? errorArg.message : 'Invalid Tasmota configuration.' };
|
|
80
|
-
}
|
|
143
|
+
} finally { pending = false; }
|
|
81
144
|
},
|
|
82
145
|
};
|
|
83
146
|
}
|
|
@@ -50,9 +50,9 @@ export class TasmotaMapper {
|
|
|
50
50
|
return sensors;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
public static device(deviceArg: ITasmotaSelectedDevice, stateArg: ITasmotaDeviceState, connectedArg: boolean): plugins.shxInterfaces.data.IDeviceDefinition {
|
|
54
|
-
const features: plugins.shxInterfaces.data.IDeviceFeature[] = deviceArg.relays.map((relayArg) => ({ id: `power_${relayArg}`, capability: 'switch', name: `Relay ${relayArg}`, readable: true, writable:
|
|
55
|
-
if (deviceArg.brightness) features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable:
|
|
53
|
+
public static device(deviceArg: ITasmotaSelectedDevice, stateArg: ITasmotaDeviceState, connectedArg: boolean, enableControlsArg = false): plugins.shxInterfaces.data.IDeviceDefinition {
|
|
54
|
+
const features: plugins.shxInterfaces.data.IDeviceFeature[] = deviceArg.relays.map((relayArg) => ({ id: `power_${relayArg}`, capability: 'switch', name: `Relay ${relayArg}`, readable: true, writable: enableControlsArg }));
|
|
55
|
+
if (deviceArg.brightness) features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable: enableControlsArg, unit: '%' });
|
|
56
56
|
for (const id of Object.keys(stateArg.sensors).sort()) features.push({ id, capability: id.startsWith('sensor_ENERGY/') ? 'energy' : 'sensor', name: id.slice('sensor_'.length).split('/').map(decodeURIComponent).join(' '), readable: true, writable: false });
|
|
57
57
|
const values = { ...stateArg.controls, ...stateArg.sensors };
|
|
58
58
|
return {
|
|
@@ -69,12 +69,12 @@ export class TasmotaMapper {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
public static entities(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition): IIntegrationEntity[] {
|
|
72
|
+
public static entities(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition, homeIdArg: string): IIntegrationEntity[] {
|
|
73
73
|
return deviceArg.features.map((featureArg) => {
|
|
74
74
|
const platform = featureArg.capability === 'switch' ? 'switch' : featureArg.capability === 'light' ? 'light' : 'sensor';
|
|
75
75
|
return {
|
|
76
|
-
id: `${platform}.${encodeURIComponent(deviceArg.id
|
|
77
|
-
uniqueId:
|
|
76
|
+
id: `${encodeURIComponent(homeIdArg)}:${platform}.${encodeURIComponent(JSON.stringify([deviceArg.id, featureArg.id]))}`,
|
|
77
|
+
uniqueId: JSON.stringify([homeIdArg, deviceArg.id, featureArg.id]),
|
|
78
78
|
integrationDomain: 'tasmota',
|
|
79
79
|
deviceId: deviceArg.id,
|
|
80
80
|
platform,
|
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
import type { IMqttConfig } from '../mqtt/mqtt.types.js';
|
|
2
|
+
import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
|
|
3
|
+
|
|
4
|
+
export interface ITasmotaBrokerIdentity {
|
|
5
|
+
readonly host: string;
|
|
6
|
+
readonly port: number;
|
|
7
|
+
readonly protocol: 'mqtt' | 'mqtts';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ITasmotaCredential extends ITasmotaBrokerIdentity {
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
readonly username?: string;
|
|
13
|
+
readonly password?: string;
|
|
14
|
+
readonly ca?: string;
|
|
15
|
+
readonly cert?: string;
|
|
16
|
+
readonly key?: string;
|
|
17
|
+
readonly servername?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ITasmotaCredentialRequest {
|
|
21
|
+
readonly homeId: string;
|
|
22
|
+
readonly credentialRef: string;
|
|
23
|
+
readonly broker: ITasmotaBrokerIdentity;
|
|
24
|
+
readonly signal: AbortSignal;
|
|
25
|
+
/** Absolute deadline on performance.now(), not wall-clock time. */
|
|
26
|
+
readonly deadlineMonotonicMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TTasmotaCredentialResolver = (requestArg: ITasmotaCredentialRequest) => Promise<ITasmotaCredential>;
|
|
30
|
+
|
|
31
|
+
export interface ITasmotaIntegrationOptions extends INativeIntegrationOwnerOptions {
|
|
32
|
+
/** Runtime-only embedding-owner permission. Defaults to false. */
|
|
33
|
+
readonly enableControls?: boolean;
|
|
34
|
+
readonly credentialResolver?: TTasmotaCredentialResolver;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TTasmotaAuthentication = { readonly mode: 'anonymous' }
|
|
38
|
+
| { readonly mode: 'credential-reference'; readonly credentialRef: string };
|
|
2
39
|
|
|
3
40
|
export interface ITasmotaDeviceConfig {
|
|
4
41
|
topic: string;
|
|
@@ -14,7 +51,9 @@ export interface ITasmotaDeviceConfig {
|
|
|
14
51
|
}
|
|
15
52
|
|
|
16
53
|
export interface IHomeAssistantTasmotaConfig {
|
|
17
|
-
|
|
54
|
+
readonly homeId: string;
|
|
55
|
+
broker: Omit<IMqttConfig, 'username' | 'password'> & { host: string };
|
|
56
|
+
readonly authentication: TTasmotaAuthentication;
|
|
18
57
|
devices: ITasmotaDeviceConfig[];
|
|
19
58
|
initialSnapshotTimeoutMs?: number;
|
|
20
59
|
}
|
|
@@ -34,7 +73,9 @@ export interface ITasmotaSelectedDevice {
|
|
|
34
73
|
}
|
|
35
74
|
|
|
36
75
|
export interface ITasmotaConfig {
|
|
37
|
-
|
|
76
|
+
readonly homeId: string;
|
|
77
|
+
readonly authentication: TTasmotaAuthentication;
|
|
78
|
+
broker: Omit<IMqttConfig, 'username' | 'password'> & { host: string };
|
|
38
79
|
devices: ITasmotaSelectedDevice[];
|
|
39
80
|
initialSnapshotTimeoutMs: number;
|
|
40
81
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tuya local protocol 3.4
|
|
2
2
|
|
|
3
3
|
`TuyaIntegration` and the existing `HomeAssistantTuyaIntegration` export provide a
|
|
4
|
-
native local runtime through published `@ecobridge.xyz/devicemanager` 4.
|
|
4
|
+
native local runtime through published `@ecobridge.xyz/devicemanager` 4.11.1.
|
|
5
5
|
Setup requires an explicit home, device ID, literal private/link-local/loopback
|
|
6
6
|
IPv4 endpoint, local key and this product's verified datapoint schema. Discovery
|
|
7
7
|
has no broadcast probes and makes no claim to locate devices or obtain credentials.
|
|
@@ -33,11 +33,12 @@ const config: IHomeAssistantTuyaConfig = {
|
|
|
33
33
|
{ id: 108, name: 'Temperature', kind: 'sensor', valueType: 'integer', scale: 1, unit: '°C' },
|
|
34
34
|
],
|
|
35
35
|
};
|
|
36
|
-
const integration = new TuyaIntegration();
|
|
36
|
+
const integration = new TuyaIntegration({ homeId: 'home-one' });
|
|
37
|
+
const context = Object.freeze({ homeId: 'home-one', configStore: store });
|
|
37
38
|
// One explicit provisioning action stores configuration and the opaque 16-byte
|
|
38
39
|
// local key together, encrypted under the caller's key provider.
|
|
39
|
-
await integration.provisionLocalKey(config, configuredLocalKeyBytes,
|
|
40
|
-
const runtime = await integration.setup(config,
|
|
40
|
+
await integration.provisionLocalKey(config, configuredLocalKeyBytes, context);
|
|
41
|
+
const runtime = await integration.setup(config, context);
|
|
41
42
|
try {
|
|
42
43
|
const [device] = await runtime.devices();
|
|
43
44
|
const power = device.features.find((feature) => feature.id === 'dp_7');
|
|
@@ -52,6 +53,14 @@ try {
|
|
|
52
53
|
}
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
The host must independently select `context.homeId` for provisioning and setup.
|
|
57
|
+
It must match the configuration home and the actual store scope; it is never
|
|
58
|
+
inferred from either. An optional constructor `homeId` constrains the instance.
|
|
59
|
+
Otherwise, the first valid synchronous admission fixes its home even if subsequent
|
|
60
|
+
I/O fails. Configuration, context fields and local-key bytes are captured before
|
|
61
|
+
asynchronous work. Accessors and overridden store-scope methods cannot grant
|
|
62
|
+
admission. The host still owns authenticated access to the selected home.
|
|
63
|
+
|
|
55
64
|
The store must be an actual `NoSqlConfigStore` scoped to the exact configured home
|
|
56
65
|
and `tuya` domain. Generic stores are rejected. Configuration and credentials use
|
|
57
66
|
`device:<configured device ID>` in that encrypted scope; setup rejects a different
|
|
@@ -85,14 +94,38 @@ arrive. Failures retain `effectOutcome: 'not-sent'`, `'rejected'` or `'indetermi
|
|
|
85
94
|
a transmitted effect with a lost/late response may already have happened. There is
|
|
86
95
|
no retry, retransmission or reconnect. Lifetime/setup cancellation and feature-write
|
|
87
96
|
signals are supported. `destroy()` is permanent, idempotent and drains setup and
|
|
88
|
-
protocol operations
|
|
89
|
-
|
|
97
|
+
protocol operations and admitted asynchronous subscriber continuations. A fulfilled
|
|
98
|
+
provisioning write remains fulfilled if cancellation or close arrives during its
|
|
99
|
+
persistence; close joins the actual write result and never replays it. Setup and
|
|
100
|
+
provisioning are reserved before store or protocol callbacks run. Destruction
|
|
101
|
+
publishes one stable close promise before abort callbacks run. Caller cancellation
|
|
102
|
+
also closes promoted runtime ownership and releases its slot after cleanup.
|
|
103
|
+
Use a new integration for a new lifetime. Cached reads after
|
|
104
|
+
shutdown remain unavailable and read-only with their original report timestamps.
|
|
90
105
|
|
|
91
|
-
Subscriptions are bounded to 128 listeners
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
Subscriptions are bounded to 128 listeners and 128 outstanding callback
|
|
107
|
+
continuations, abortable and explicitly removable. Handlers must not block or
|
|
108
|
+
await destruction of their own runtime: destruction joins their continuation.
|
|
109
|
+
Returned promises and thenables are owned and joined even after listener removal.
|
|
110
|
+
Callback failures are retained through cleanup and logged with a generic,
|
|
111
|
+
credential-free message. Overflow closes the runtime and retains the failure.
|
|
94
112
|
All listeners are removed during destruction.
|
|
95
|
-
|
|
113
|
+
|
|
114
|
+
The shared native owner accounts for pending setup/provisioning operations,
|
|
115
|
+
promoted runtimes and retained failed cleanup. Its default limit is 16; the optional
|
|
116
|
+
constructor `capacity` selects a positive safe-integer limit. A failed cleanup
|
|
117
|
+
retains its capacity slot and rejects destruction. `integration.retryCleanup()`
|
|
118
|
+
retries only failed cleanup handles and explicitly acknowledges already-drained
|
|
119
|
+
subscriber failures; it never reruns callbacks, provisioning, queries or controls,
|
|
120
|
+
and never reopens a session. It does not change the original rejected close promise.
|
|
121
|
+
A resource whose owning cleanup still rejects remains retained.
|
|
122
|
+
|
|
123
|
+
Per-DP provisioned `read-write` access continues to grant control. There is no
|
|
124
|
+
additional host control-permission API and no automatic preflight or post-write
|
|
125
|
+
readback. Those public-contract changes require a separate product decision, as
|
|
126
|
+
does a config flow. Protocol authentication proves possession of the local key;
|
|
127
|
+
identity fields are checked when present, but this is not independent hardware
|
|
128
|
+
attestation. Firmware qualification remains separate from fixture verification.
|
|
96
129
|
|
|
97
130
|
Other protocol versions, cloud-only and sleeping devices, gateway children, BLE,
|
|
98
131
|
Zigbee, raw/bitmap/enum mappings and arbitrary actions are unsupported. The shared
|
|
@@ -101,6 +134,15 @@ device protocol union currently has no Tuya member, so the generic device report
|
|
|
101
134
|
loopback fixture tests cover the implementation; qualification against real product
|
|
102
135
|
firmware requires separately authorized hardware access.
|
|
103
136
|
|
|
137
|
+
Fixture qualification uses `pnpm exec tstest test/tuya/ --verbose --logfile --timeout 60`:
|
|
138
|
+
`test.tuya.node.ts` contains 28 encrypted persistence/protocol cases and
|
|
139
|
+
`test.tuya.owner.node.ts` adds 22 admission, reentry, cancellation, capacity,
|
|
140
|
+
accepted-persistence, retained-cleanup and callback-continuation regressions.
|
|
141
|
+
The 50-case suite passes with no device or cloud account. Strict domain/test checks
|
|
142
|
+
use `strict: true`, `noImplicitAny: true` and `skipLibCheck: false`; production
|
|
143
|
+
build and the complete test type check are also required. This fixture evidence
|
|
144
|
+
does not establish physical acceptance for a particular product or firmware.
|
|
145
|
+
|
|
104
146
|
Product types and permissions follow the vendor's [DP Model and Control Protocol](https://developer.tuya.com/en/docs/iot-device-dev/TuyaOS-iot_abi_dp_ctrl?id=Kcoglhn5r7ajr).
|
|
105
147
|
LAN framing is implemented by the owning dependency, whose public API and wire
|
|
106
148
|
contract are documented in its [readme](https://code.foss.global/ecobridge.xyz/devicemanager).
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
|
3
|
+
import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
|
|
3
4
|
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
4
5
|
import type { IIntegrationRuntime, IIntegrationSetupContext } from '../../core/types.js';
|
|
5
6
|
import { TuyaRuntime } from './tuya.classes.runtime.js';
|
|
6
|
-
import type { IHomeAssistantTuyaConfig, ITuyaStoredConfiguration } from './tuya.types.js';
|
|
7
|
-
import { normalizeTuyaConfig,
|
|
7
|
+
import type { IHomeAssistantTuyaConfig, ITuyaIntegrationOptions, ITuyaStoredConfiguration } from './tuya.types.js';
|
|
8
|
+
import { normalizeTuyaConfig, tuyaSetupContext, tuyaCredentialId, tuyaKeys, tuyaRecord } from './tuya.validation.js';
|
|
8
9
|
|
|
9
10
|
export class TuyaIntegration extends BaseIntegration<IHomeAssistantTuyaConfig> {
|
|
10
11
|
public readonly domain = 'tuya';
|
|
@@ -12,118 +13,88 @@ export class TuyaIntegration extends BaseIntegration<IHomeAssistantTuyaConfig> {
|
|
|
12
13
|
public readonly status = 'control-runtime' as const;
|
|
13
14
|
public readonly discoveryDescriptor = new DiscoveryDescriptor({ integrationDomain: 'tuya', displayName: 'Tuya 3.4 local device' });
|
|
14
15
|
public readonly metadata = Object.freeze({ source: 'native-typescript', integrationType: 'device', iotClass: 'local_push', protocolVersion: '3.4' });
|
|
15
|
-
readonly #
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
readonly #owner: NativeIntegrationOwner;
|
|
17
|
+
|
|
18
|
+
constructor(optionsArg: ITuyaIntegrationOptions = {}) {
|
|
19
|
+
super();
|
|
20
|
+
this.#owner = new NativeIntegrationOwner(this.domain, optionsArg);
|
|
21
|
+
}
|
|
20
22
|
|
|
21
23
|
public provisionLocalKey(configArg: IHomeAssistantTuyaConfig, localKeyArg: Uint8Array, contextArg: IIntegrationSetupContext): Promise<void> {
|
|
22
|
-
this.#assertOpen();
|
|
23
|
-
this.#assertCapacity();
|
|
24
24
|
const config = normalizeTuyaConfig(configArg);
|
|
25
|
-
const
|
|
25
|
+
const context = tuyaSetupContext(contextArg);
|
|
26
26
|
if (!(localKeyArg instanceof Uint8Array) || localKeyArg.byteLength !== 16) throw new TypeError('Tuya local key must contain exactly 16 opaque bytes.');
|
|
27
27
|
const copy = Buffer.from(localKeyArg);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
signal.throwIfAborted();
|
|
34
|
-
await store.initialize({ signal });
|
|
35
|
-
this.#assertOpen();
|
|
36
|
-
signal.throwIfAborted();
|
|
28
|
+
try {
|
|
29
|
+
return this.#owner.runOperation(context, config.homeId, async (leaseArg) => {
|
|
30
|
+
const store = leaseArg.context.configStore!;
|
|
31
|
+
await store.initialize({ signal: leaseArg.signal });
|
|
32
|
+
leaseArg.assertOpen();
|
|
37
33
|
await store.set<ITuyaStoredConfiguration>(tuyaCredentialId(config.deviceId), { version: 1, config, localKey: copy.toString('base64') });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
})();
|
|
42
|
-
return this.#track(operation);
|
|
34
|
+
// A fulfilled admitted write stays fulfilled even if close/cancellation arrives during persistence.
|
|
35
|
+
}).finally(() => copy.fill(0));
|
|
36
|
+
} catch (errorArg) { copy.fill(0); throw errorArg; }
|
|
43
37
|
}
|
|
44
38
|
|
|
45
39
|
public setup(configArg: IHomeAssistantTuyaConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
|
|
46
|
-
this.#assertOpen();
|
|
47
|
-
this.#assertCapacity();
|
|
48
40
|
const config = normalizeTuyaConfig(configArg);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
const context = tuyaSetupContext(contextArg);
|
|
42
|
+
return this.#owner.runSetup(context, config.homeId, async (leaseArg) => {
|
|
43
|
+
const store = leaseArg.context.configStore!;
|
|
44
|
+
const signal = leaseArg.signal;
|
|
45
|
+
await store.initialize({ signal });
|
|
46
|
+
leaseArg.assertOpen();
|
|
47
|
+
const stored = tuyaRecord(await store.get<unknown>(tuyaCredentialId(config.deviceId)), 'stored configuration');
|
|
48
|
+
tuyaKeys(stored, ['version', 'config', 'localKey']);
|
|
49
|
+
if (stored.version !== 1 || JSON.stringify(normalizeTuyaConfig(stored.config)) !== JSON.stringify(config)) {
|
|
50
|
+
throw new TypeError('Tuya configuration does not match its encrypted provisioned configuration.');
|
|
51
|
+
}
|
|
52
|
+
if (typeof stored.localKey !== 'string' || stored.localKey.length !== 24) throw new TypeError('Tuya stored local key is invalid.');
|
|
53
|
+
const key = Buffer.from(stored.localKey, 'base64');
|
|
54
|
+
let protocol: plugins.ecobridgeDeviceManager.TuyaProtocol;
|
|
55
|
+
let runtime: TuyaRuntime | undefined;
|
|
55
56
|
let unsubscribeAdmission: (() => void) | undefined;
|
|
57
|
+
let cleanupAttempted = false;
|
|
56
58
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new TypeError('Tuya configuration does not match its encrypted provisioned configuration.');
|
|
64
|
-
}
|
|
65
|
-
if (typeof stored.localKey !== 'string' || stored.localKey.length !== 24) throw new TypeError('Tuya stored local key is invalid.');
|
|
66
|
-
const key = Buffer.from(stored.localKey, 'base64');
|
|
67
|
-
try {
|
|
68
|
-
if (key.byteLength !== 16 || key.toString('base64') !== stored.localKey) throw new TypeError('Tuya stored local key is invalid.');
|
|
69
|
-
signal.throwIfAborted();
|
|
70
|
-
protocol = new plugins.ecobridgeDeviceManager.TuyaProtocol({
|
|
71
|
-
deviceId: config.deviceId, localKey: key, host: config.host, port: config.port, version: config.version,
|
|
72
|
-
connectTimeoutMs: config.connectTimeoutMs, requestTimeoutMs: config.requestTimeoutMs,
|
|
73
|
-
heartbeatIntervalMs: config.heartbeatIntervalMs, abortSignal: signal,
|
|
74
|
-
});
|
|
75
|
-
} finally { key.fill(0); }
|
|
76
|
-
const reports: plugins.ecobridgeDeviceManager.ITuyaDatapointReport[] = [];
|
|
77
|
-
let admissionOverflow = false;
|
|
78
|
-
unsubscribeAdmission = protocol.subscribe((reportArg) => {
|
|
79
|
-
if (reports.length >= 128) { admissionOverflow = true; controller.abort(); }
|
|
80
|
-
else reports.push(reportArg);
|
|
59
|
+
if (key.byteLength !== 16 || key.toString('base64') !== stored.localKey) throw new TypeError('Tuya stored local key is invalid.');
|
|
60
|
+
leaseArg.assertOpen();
|
|
61
|
+
protocol = new plugins.ecobridgeDeviceManager.TuyaProtocol({
|
|
62
|
+
deviceId: config.deviceId, localKey: key, host: config.host, port: config.port, version: config.version,
|
|
63
|
+
connectTimeoutMs: config.connectTimeoutMs, requestTimeoutMs: config.requestTimeoutMs,
|
|
64
|
+
heartbeatIntervalMs: config.heartbeatIntervalMs, abortSignal: signal,
|
|
81
65
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.#destroyed = true;
|
|
109
|
-
for (const controller of this.#controllers) controller.abort();
|
|
110
|
-
this.#destroy = (async () => {
|
|
111
|
-
await Promise.allSettled([...this.#setups]);
|
|
112
|
-
await Promise.all([...this.#runtimes].map((runtimeArg) => runtimeArg.destroy()));
|
|
113
|
-
this.#controllers.clear();
|
|
114
|
-
})();
|
|
115
|
-
return this.#destroy;
|
|
66
|
+
// One composite handle transfers protocol and callback cleanup together to the runtime.
|
|
67
|
+
leaseArg.own(async () => {
|
|
68
|
+
const retry = cleanupAttempted;
|
|
69
|
+
cleanupAttempted = true;
|
|
70
|
+
unsubscribeAdmission?.();
|
|
71
|
+
unsubscribeAdmission = undefined;
|
|
72
|
+
if (runtime) await runtime.closeOwnedResources(retry);
|
|
73
|
+
else await protocol.close();
|
|
74
|
+
});
|
|
75
|
+
} finally { key.fill(0); }
|
|
76
|
+
const reports: plugins.ecobridgeDeviceManager.ITuyaDatapointReport[] = [];
|
|
77
|
+
let admissionOverflow = false;
|
|
78
|
+
unsubscribeAdmission = protocol.subscribe((reportArg) => {
|
|
79
|
+
if (reports.length >= 128) { admissionOverflow = true; void leaseArg.close().catch(() => undefined); }
|
|
80
|
+
else reports.push(reportArg);
|
|
81
|
+
});
|
|
82
|
+
await protocol.connect(signal);
|
|
83
|
+
await protocol.queryDatapoints(signal);
|
|
84
|
+
if (admissionOverflow) throw new Error('Tuya admission report capacity exceeded.');
|
|
85
|
+
leaseArg.assertOpen();
|
|
86
|
+
unsubscribeAdmission();
|
|
87
|
+
unsubscribeAdmission = undefined;
|
|
88
|
+
runtime = new TuyaRuntime(config, protocol, reports, leaseArg);
|
|
89
|
+
leaseArg.promoteToRuntime();
|
|
90
|
+
return runtime;
|
|
91
|
+
});
|
|
116
92
|
}
|
|
117
93
|
|
|
118
|
-
|
|
94
|
+
public destroy(): Promise<void> { return this.#owner.close(); }
|
|
119
95
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#track<TResult>(operationArg: Promise<TResult>): Promise<TResult> {
|
|
123
|
-
this.#setups.add(operationArg);
|
|
124
|
-
void operationArg.then(() => this.#setups.delete(operationArg), () => this.#setups.delete(operationArg));
|
|
125
|
-
return operationArg;
|
|
126
|
-
}
|
|
96
|
+
/** Retry retained cleanup only; never reopen a session or replay an accepted operation. */
|
|
97
|
+
public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
|
|
127
98
|
}
|
|
128
99
|
|
|
129
100
|
export class HomeAssistantTuyaIntegration extends TuyaIntegration {}
|