@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,48 +1,53 @@
|
|
|
1
|
-
import type { IConfigFlow,
|
|
2
|
-
import type
|
|
1
|
+
import type { IConfigFlow, IConfigFlowStep, IDiscoveryCandidate, INativeConfigFlowContext } from '../../core/types.js';
|
|
2
|
+
import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
|
|
3
|
+
import type { IShellyConfig, IShellyIntegrationOptions } from './shelly.types.js';
|
|
3
4
|
import { ShellyClient } from './shelly.classes.client.js';
|
|
5
|
+
import { normalizeShellyConfig, normalizeShellyOptions, shellyConfigFields, shellyData } from './shelly.validation.js';
|
|
4
6
|
|
|
5
7
|
export class ShellyConfigFlow implements IConfigFlow<IShellyConfig> {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
readonly #scope: NativeIntegrationScope;
|
|
9
|
+
readonly #options: Readonly<IShellyIntegrationOptions>;
|
|
10
|
+
constructor(optionsArg: IShellyIntegrationOptions = {}, scopeArg?: NativeIntegrationScope) {
|
|
11
|
+
this.#options = normalizeShellyOptions(optionsArg);
|
|
12
|
+
this.#scope = scopeArg ?? new NativeIntegrationOwner('shelly', { homeId: this.#options.homeId, capacity: this.#options.capacity }).createFlowScope();
|
|
13
|
+
}
|
|
14
|
+
public async start(candidateArg: IDiscoveryCandidate, contextArg: INativeConfigFlowContext): Promise<IConfigFlowStep<IShellyConfig>> {
|
|
15
|
+
const candidate = shellyData(candidateArg);
|
|
16
|
+
if (candidate.integrationDomain !== undefined && candidate.integrationDomain !== 'shelly') return { kind: 'error', error: 'Candidate does not belong to Shelly.' };
|
|
17
|
+
const hints = { host: candidate.host, port: candidate.port };
|
|
18
|
+
const admitted = this.#scope.admit(contextArg);
|
|
19
|
+
return { kind: 'form', title: 'Connect selected Shelly Gen2/3',
|
|
20
|
+
description: 'Confirm the reported id, MAC, model and generation for the host-selected home. Supply only a caller credential reference; output controls require explicit enablement.',
|
|
12
21
|
fields: [
|
|
13
22
|
{ name: 'host', label: 'Host', type: 'text', required: true },
|
|
14
23
|
{ name: 'port', label: 'Port', type: 'number' },
|
|
15
|
-
{ name: 'protocol', label: 'Protocol', type: 'select', options: [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ name: '
|
|
20
|
-
{ name: '
|
|
24
|
+
{ name: 'protocol', label: 'Protocol', type: 'select', options: [{ label: 'HTTP', value: 'http' }, { label: 'HTTPS', value: 'https' }] },
|
|
25
|
+
{ name: 'expectedDeviceId', label: 'Exact observed device id', type: 'text', required: true },
|
|
26
|
+
{ name: 'expectedMacAddress', label: 'Exact observed MAC (12 hex digits)', type: 'text', required: true },
|
|
27
|
+
{ name: 'expectedModel', label: 'Exact observed model', type: 'text', required: true },
|
|
28
|
+
{ name: 'expectedGeneration', label: 'Observed generation (2 or 3)', type: 'number', required: true },
|
|
29
|
+
{ name: 'credentialRef', label: 'Caller password reference (if authentication is enabled)', type: 'text' },
|
|
30
|
+
{ name: 'allowUnencryptedLan', label: 'Allow unencrypted selected LAN HTTP', type: 'boolean' },
|
|
31
|
+
{ name: 'allowControl', label: 'Enable absolute switch output controls', type: 'boolean' },
|
|
21
32
|
],
|
|
22
|
-
submit: async (
|
|
23
|
-
if (valuesArg.protocol !== undefined && !['http', 'https'].includes(String(valuesArg.protocol)) ||
|
|
24
|
-
valuesArg.host !== undefined && typeof valuesArg.host !== 'string' ||
|
|
25
|
-
valuesArg.port !== undefined && typeof valuesArg.port !== 'number' ||
|
|
26
|
-
valuesArg.username !== undefined && valuesArg.username !== 'admin' ||
|
|
27
|
-
valuesArg.password !== undefined && typeof valuesArg.password !== 'string') {
|
|
28
|
-
return { kind: 'error', error: 'Invalid Shelly connection settings.' };
|
|
29
|
-
}
|
|
30
|
-
const config: IShellyConfig = {
|
|
31
|
-
host: typeof valuesArg.host === 'string' ? valuesArg.host : candidateArg.host ?? '',
|
|
32
|
-
port: typeof valuesArg.port === 'number' ? valuesArg.port : candidateArg.port,
|
|
33
|
-
protocol: valuesArg.protocol === 'https' ? 'https' : 'http',
|
|
34
|
-
username: 'admin',
|
|
35
|
-
password: typeof valuesArg.password === 'string' ? valuesArg.password : undefined,
|
|
36
|
-
};
|
|
37
|
-
let client: ShellyClient | undefined;
|
|
33
|
+
submit: async (inputArg) => {
|
|
38
34
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
const values = shellyData(inputArg, shellyConfigFields);
|
|
36
|
+
if (values.homeId !== undefined && values.homeId !== admitted.homeId) throw new Error('Shelly submitted home conflicts with the host-selected home.');
|
|
37
|
+
const config = normalizeShellyConfig({ ...values, homeId: admitted.homeId, host: values.host ?? hints.host, port: values.port ?? hints.port });
|
|
38
|
+
return await this.#scope.run(admitted, config.homeId, async (leaseArg) => {
|
|
39
|
+
const client = new ShellyClient(config, { ...leaseArg.context, abortSignal: leaseArg.signal }, this.#options);
|
|
40
|
+
leaseArg.own(() => client.retryCleanup());
|
|
41
|
+
await client.getSnapshot();
|
|
42
|
+
leaseArg.assertOpen();
|
|
43
|
+
return { kind: 'done', title: 'Selected Shelly admitted', config };
|
|
44
|
+
});
|
|
45
|
+
} catch (errorArg) {
|
|
46
|
+
if (errorArg instanceof AggregateError) throw errorArg;
|
|
47
|
+
return { kind: 'error', error: 'Could not validate selected Shelly home, identity, capabilities and credential reference.' };
|
|
48
|
+
}
|
|
49
|
+
} };
|
|
47
50
|
}
|
|
51
|
+
public destroy(): Promise<void> { return this.#scope.close(); }
|
|
52
|
+
public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
|
|
48
53
|
}
|
|
@@ -1,99 +1,73 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
|
3
|
-
import type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
IIntegrationEvent,
|
|
7
|
-
IIntegrationRuntime,
|
|
8
|
-
IIntegrationSetupContext,
|
|
9
|
-
IServiceCallRequest,
|
|
10
|
-
IServiceCallResult,
|
|
11
|
-
} from '../../core/types.js';
|
|
3
|
+
import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
|
|
4
|
+
import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime,
|
|
5
|
+
INativeIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
|
|
12
6
|
import { ShellyClient } from './shelly.classes.client.js';
|
|
13
7
|
import { ShellyConfigFlow } from './shelly.classes.configflow.js';
|
|
14
8
|
import { createShellyDiscoveryDescriptor } from './shelly.discovery.js';
|
|
15
9
|
import { ShellyMapper } from './shelly.mapper.js';
|
|
16
|
-
import
|
|
10
|
+
import { normalizeShellyConfig, normalizeShellyOptions, shellyData } from './shelly.validation.js';
|
|
11
|
+
import { ShellyOperationError, type IShellyConfig, type IShellyIntegrationOptions, type IShellySnapshot,
|
|
12
|
+
type TShellyClientEvent } from './shelly.types.js';
|
|
17
13
|
|
|
18
14
|
export class ShellyIntegration extends BaseIntegration<IShellyConfig> {
|
|
19
15
|
public readonly domain = 'shelly';
|
|
20
16
|
public readonly displayName = 'Shelly';
|
|
21
17
|
public readonly status = 'control-runtime' as const;
|
|
22
18
|
public readonly discoveryDescriptor = createShellyDiscoveryDescriptor();
|
|
23
|
-
public readonly configFlow
|
|
24
|
-
public readonly metadata = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
integrationType: 'device',
|
|
29
|
-
iotClass: 'local_push',
|
|
30
|
-
qualityScale: 'platinum',
|
|
31
|
-
};
|
|
32
|
-
private readonly runtimes = new Set<ShellyRuntime>();
|
|
33
|
-
private readonly pendingClients = new Set<ShellyClient>();
|
|
34
|
-
private destroyed = false;
|
|
19
|
+
public readonly configFlow: ShellyConfigFlow;
|
|
20
|
+
public readonly metadata = { source: 'native-typescript', upstreamPath: 'homeassistant/components/shelly',
|
|
21
|
+
upstreamDomain: 'shelly', integrationType: 'device', iotClass: 'local_push', qualityScale: 'platinum' };
|
|
22
|
+
readonly #owner: NativeIntegrationOwner;
|
|
23
|
+
readonly #options: Readonly<IShellyIntegrationOptions>;
|
|
35
24
|
|
|
36
|
-
public
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
const snapshot = await client.getSnapshot();
|
|
42
|
-
if (this.destroyed) throw new Error('Shelly integration has been destroyed.');
|
|
43
|
-
const runtime = new ShellyRuntime(client, snapshot, () => this.runtimes.delete(runtime));
|
|
44
|
-
this.runtimes.add(runtime);
|
|
45
|
-
this.pendingClients.delete(client);
|
|
46
|
-
return runtime;
|
|
47
|
-
} catch (errorArg) {
|
|
48
|
-
try { await client.destroy(); }
|
|
49
|
-
catch (cleanupErrorArg) { throw new AggregateError([errorArg, cleanupErrorArg], 'Shelly setup and cleanup failed.'); }
|
|
50
|
-
this.pendingClients.delete(client);
|
|
51
|
-
throw errorArg;
|
|
52
|
-
}
|
|
25
|
+
public constructor(optionsArg: IShellyIntegrationOptions = {}) {
|
|
26
|
+
super();
|
|
27
|
+
this.#options = normalizeShellyOptions(optionsArg);
|
|
28
|
+
this.#owner = new NativeIntegrationOwner('shelly', { homeId: this.#options.homeId, capacity: this.#options.capacity });
|
|
29
|
+
this.configFlow = new ShellyConfigFlow(this.#options, this.#owner.createFlowScope());
|
|
53
30
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
31
|
+
public setup(configArg: IShellyConfig, contextArg: INativeIntegrationSetupContext): Promise<IIntegrationRuntime> {
|
|
32
|
+
try {
|
|
33
|
+
const config = normalizeShellyConfig(configArg);
|
|
34
|
+
return this.#owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
|
|
35
|
+
const client = new ShellyClient(config, { ...leaseArg.context, abortSignal: leaseArg.signal }, this.#options);
|
|
36
|
+
leaseArg.own(() => client.retryCleanup());
|
|
37
|
+
const snapshot = await client.getSnapshot();
|
|
38
|
+
leaseArg.assertOpen();
|
|
39
|
+
const runtime = new ShellyRuntime(client, snapshot, leaseArg);
|
|
40
|
+
leaseArg.promoteToRuntime();
|
|
41
|
+
return runtime;
|
|
42
|
+
});
|
|
43
|
+
} catch (errorArg) { return Promise.reject(errorArg); }
|
|
66
44
|
}
|
|
45
|
+
public destroy(): Promise<void> { return this.#owner.close(); }
|
|
46
|
+
public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
|
|
67
47
|
}
|
|
68
|
-
|
|
69
48
|
export class HomeAssistantShellyIntegration extends ShellyIntegration {}
|
|
70
49
|
|
|
71
50
|
class ShellyRuntime implements IIntegrationRuntime {
|
|
72
51
|
public readonly domain = 'shelly';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.snapshot = await this.client.getSnapshot(abortSignalArg);
|
|
80
|
-
return ShellyMapper.toDevices(this.snapshot).map((deviceArg) => ({
|
|
81
|
-
...deviceArg,
|
|
82
|
-
online: this.isHealthy(),
|
|
52
|
+
constructor(private readonly client: ShellyClient, private snapshot: IShellySnapshot, private readonly lease: INativeSetupLease) {}
|
|
53
|
+
public isHealthy(): boolean { return !this.lease.signal.aborted && this.client.isHealthy(); }
|
|
54
|
+
public async devices(signalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
|
|
55
|
+
this.lease.assertOpen();
|
|
56
|
+
this.snapshot = await this.client.getSnapshot(signalArg);
|
|
57
|
+
return ShellyMapper.toDevices(this.snapshot).map((deviceArg) => ({ ...deviceArg, online: this.isHealthy(),
|
|
83
58
|
state: deviceArg.state.map((stateArg) => stateArg.featureId === 'connectivity' ?
|
|
84
|
-
{ ...stateArg, value: this.isHealthy() ? 'online' : 'offline' } : stateArg)
|
|
85
|
-
}));
|
|
59
|
+
{ ...stateArg, value: this.isHealthy() ? 'online' : 'offline' } : stateArg) }));
|
|
86
60
|
}
|
|
87
|
-
|
|
88
61
|
public async entities(): Promise<IIntegrationEntity[]> {
|
|
62
|
+
this.lease.assertOpen();
|
|
89
63
|
this.snapshot = await this.client.getSnapshot();
|
|
90
64
|
return ShellyMapper.toEntities(this.snapshot).map((entityArg) => ({ ...entityArg, available: this.isHealthy() }));
|
|
91
65
|
}
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, signalArg?: AbortSignal): Promise<() => Promise<void>> {
|
|
67
|
+
this.lease.assertOpen();
|
|
94
68
|
let previousEntities = ShellyMapper.toEntities(this.snapshot);
|
|
95
69
|
return this.client.subscribe((eventArg: TShellyClientEvent) => {
|
|
96
|
-
const deviceId = ShellyMapper.
|
|
70
|
+
const deviceId = ShellyMapper.deviceId(this.snapshot);
|
|
97
71
|
const emit = (event: Omit<IIntegrationEvent, 'integrationDomain' | 'deviceId' | 'timestamp'>): void =>
|
|
98
72
|
handlerArg({ ...event, integrationDomain: this.domain, deviceId, timestamp: eventArg.timestamp });
|
|
99
73
|
if (eventArg.type === 'status') {
|
|
@@ -103,54 +77,61 @@ class ShellyRuntime implements IIntegrationRuntime {
|
|
|
103
77
|
const previous = previousEntities.find((candidateArg) => candidateArg.id === entity.id);
|
|
104
78
|
if (JSON.stringify(previous) !== JSON.stringify(entity)) emit({ type: previous ? 'state_changed' : 'entity_added', entityId: entity.id, data: { entity } });
|
|
105
79
|
}
|
|
106
|
-
for (const previous of previousEntities) {
|
|
107
|
-
if (!entities.some((entityArg) => entityArg.id === previous.id)) emit({ type: 'entity_removed', entityId: previous.id });
|
|
108
|
-
}
|
|
80
|
+
for (const previous of previousEntities) if (!entities.some((entityArg) => entityArg.id === previous.id)) emit({ type: 'entity_removed', entityId: previous.id });
|
|
109
81
|
previousEntities = entities;
|
|
110
|
-
} else if (eventArg.type === 'availability') {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
} else {
|
|
115
|
-
emit({ type: 'state_changed', data: { vendor: 'shelly', event: eventArg.event } });
|
|
116
|
-
}
|
|
117
|
-
}, abortSignalArg);
|
|
82
|
+
} else if (eventArg.type === 'availability') emit({ type: 'availability_changed', data: { available: eventArg.available } });
|
|
83
|
+
else if (eventArg.type === 'error') emit({ type: 'error', data: { message: eventArg.error.message } });
|
|
84
|
+
else emit({ type: 'state_changed', data: { vendor: 'shelly', event: eventArg.event } });
|
|
85
|
+
}, signalArg);
|
|
118
86
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
87
|
+
public async callService(inputArg: IServiceCallRequest): Promise<IServiceCallResult> {
|
|
88
|
+
this.lease.assertOpen();
|
|
89
|
+
let request: Record<string, unknown>;
|
|
90
|
+
let target: Record<string, unknown>;
|
|
91
|
+
try {
|
|
92
|
+
request = shellyData(inputArg, ['domain', 'service', 'target', 'data']);
|
|
93
|
+
target = shellyData(request.target, ['deviceId', 'entityId']);
|
|
94
|
+
if (request.domain !== 'switch' || !['turn_on', 'turn_off'].includes(String(request.service)) ||
|
|
95
|
+
request.data !== undefined && Object.keys(shellyData(request.data)).length) return { success: false, error: 'Shelly supports only absolute switch.turn_on and switch.turn_off without extra data.' };
|
|
96
|
+
} catch { return { success: false, error: 'Invalid Shelly service request.' }; }
|
|
97
|
+
const deviceId = ShellyMapper.deviceId(this.snapshot);
|
|
98
|
+
if (target.deviceId !== undefined && target.deviceId !== deviceId) return { success: false, error: 'Shelly service target does not belong to the connected home and device.' };
|
|
126
99
|
const switches = ShellyMapper.toEntities(this.snapshot).filter((entityArg) => entityArg.platform === 'switch');
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (!
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
return { success: true };
|
|
100
|
+
const selected = target.entityId !== undefined ? switches.find((entityArg) => entityArg.id === target.entityId) :
|
|
101
|
+
target.deviceId === deviceId && switches.length === 1 ? switches[0] : undefined;
|
|
102
|
+
if (!selected) return { success: false, error: 'Shelly service requires an exact switch entity or an unambiguous device target.' };
|
|
103
|
+
const match = /\.switch_(\d+)$/.exec(selected.uniqueId);
|
|
104
|
+
if (!match) return { success: false, error: 'Shelly switch target has no component id.' };
|
|
105
|
+
return this.control(Number(match[1]), request.service === 'turn_on');
|
|
134
106
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
107
|
+
public async writeFeature(inputArg: IDeviceFeatureWriteRequest, signalArg?: AbortSignal): Promise<IServiceCallResult> {
|
|
108
|
+
this.lease.assertOpen();
|
|
109
|
+
let request: Record<string, unknown>;
|
|
110
|
+
let deviceInput: Record<string, unknown>;
|
|
111
|
+
let featureInput: Record<string, unknown>;
|
|
112
|
+
try {
|
|
113
|
+
request = shellyData(inputArg, ['device', 'feature', 'value']);
|
|
114
|
+
deviceInput = shellyData(request.device);
|
|
115
|
+
featureInput = shellyData(request.feature);
|
|
116
|
+
} catch { return { success: false, error: 'Invalid Shelly feature request.' }; }
|
|
117
|
+
if (deviceInput.id !== ShellyMapper.deviceId(this.snapshot) || deviceInput.integrationDomain !== this.domain) return { success: false, error: 'Shelly feature target does not belong to the connected home and device.' };
|
|
118
|
+
const feature = ShellyMapper.toDevices(this.snapshot)[0].features.find((entryArg) => entryArg.id === featureInput.id);
|
|
141
119
|
const match = feature && /^switch_(\d+)_output$/.exec(feature.id);
|
|
142
|
-
if (!match || !feature.writable || feature.capability !== 'switch' || typeof
|
|
143
|
-
|
|
144
|
-
return { success: true };
|
|
120
|
+
if (!match || !feature.writable || feature.capability !== 'switch' || featureInput.capability !== 'switch' || typeof request.value !== 'boolean') return { success: false, error: 'Shelly writes require a current writable switch output and boolean value.' };
|
|
121
|
+
return this.control(Number(match[1]), request.value, signalArg);
|
|
145
122
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
123
|
+
public destroy(): Promise<void> { return this.lease.close(); }
|
|
124
|
+
private async control(idArg: number, onArg: boolean, signalArg?: AbortSignal): Promise<IServiceCallResult> {
|
|
125
|
+
try {
|
|
126
|
+
const receipt = await this.client.setSwitchOutput(idArg, onArg, signalArg);
|
|
127
|
+
this.snapshot = receipt.snapshot;
|
|
128
|
+
return { success: receipt.outcome === 'observed-target',
|
|
129
|
+
...(receipt.outcome === 'indeterminate' ? { error: 'Shelly acknowledged the command, but independent reported output differs.' } : {}),
|
|
130
|
+
data: { ...receipt, devices: ShellyMapper.toDevices(receipt.snapshot), entities: ShellyMapper.toEntities(receipt.snapshot) } };
|
|
131
|
+
} catch (errorArg) {
|
|
132
|
+
const error = errorArg instanceof ShellyOperationError ? errorArg : undefined;
|
|
133
|
+
return { success: false, error: error?.message ?? 'Shelly control was not admitted.',
|
|
134
|
+
data: { effectOutcome: error?.effectOutcome ?? 'not-sent', commandAcknowledged: error?.commandAcknowledged ?? false, causalityConfirmed: false } };
|
|
135
|
+
}
|
|
155
136
|
}
|
|
156
137
|
}
|
|
@@ -28,6 +28,8 @@ export class ShellySubscription {
|
|
|
28
28
|
private resolveStart?: () => void;
|
|
29
29
|
private rejectStart?: (errorArg: Error) => void;
|
|
30
30
|
private closePromise?: Promise<void>;
|
|
31
|
+
private stopPromise?: Promise<void>;
|
|
32
|
+
private retryPromise?: Promise<void>;
|
|
31
33
|
private resolveClose?: () => void;
|
|
32
34
|
private deadline?: ReturnType<typeof setTimeout>;
|
|
33
35
|
private requestId = 1;
|
|
@@ -65,7 +67,26 @@ export class ShellySubscription {
|
|
|
65
67
|
return this.startPromise;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
public
|
|
70
|
+
public stop(): Promise<void> {
|
|
71
|
+
if (!this.stopPromise) {
|
|
72
|
+
// Publish before availability callbacks or synchronous socket/abort listeners.
|
|
73
|
+
this.stopPromise = Promise.resolve().then(() => this.drainClose());
|
|
74
|
+
this.stopping = true;
|
|
75
|
+
}
|
|
76
|
+
return this.stopPromise;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public retryCleanup(): Promise<void> {
|
|
80
|
+
if (!this.stopPromise) return this.stop();
|
|
81
|
+
if (!this.retryPromise) {
|
|
82
|
+
this.retryPromise = Promise.resolve().then(() => this.drainClose());
|
|
83
|
+
const settled = (): void => { this.retryPromise = undefined; };
|
|
84
|
+
void this.retryPromise.then(settled, settled);
|
|
85
|
+
}
|
|
86
|
+
return this.retryPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async drainClose(): Promise<void> {
|
|
69
90
|
this.stopping = true;
|
|
70
91
|
this.setHealthy(false);
|
|
71
92
|
this.clearDeadline();
|
|
@@ -123,6 +144,7 @@ export class ShellySubscription {
|
|
|
123
144
|
this.resolveStart?.();
|
|
124
145
|
this.resolveStart = undefined;
|
|
125
146
|
this.rejectStart = undefined;
|
|
147
|
+
this.options.password = undefined;
|
|
126
148
|
} catch (errorArg) {
|
|
127
149
|
this.fail(errorArg instanceof AuthenticationError || errorArg instanceof DeviceCommunicationError ?
|
|
128
150
|
errorArg : new DeviceCommunicationError('Shelly returned an invalid WebSocket payload.'));
|
|
@@ -214,5 +236,6 @@ export class ShellySubscription {
|
|
|
214
236
|
this.socket?.removeEventListener('close', this.handleClose);
|
|
215
237
|
this.resolveClose?.();
|
|
216
238
|
this.resolveClose = undefined;
|
|
239
|
+
this.options.password = undefined;
|
|
217
240
|
}
|
|
218
241
|
}
|
|
@@ -4,7 +4,7 @@ import type { IShellyDeviceConfig, IShellyDeviceInfo, IShellySnapshot, IShellySt
|
|
|
4
4
|
|
|
5
5
|
export class ShellyMapper {
|
|
6
6
|
public static toDevices(snapshotArg: IShellySnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
|
|
7
|
-
const deviceId = this.deviceId(snapshotArg
|
|
7
|
+
const deviceId = this.deviceId(snapshotArg);
|
|
8
8
|
const updatedAt = new Date().toISOString();
|
|
9
9
|
const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
|
|
10
10
|
{ id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
|
|
@@ -21,7 +21,8 @@ export class ShellyMapper {
|
|
|
21
21
|
for (const switchComponent of this.switchComponents(snapshotArg.status)) {
|
|
22
22
|
const prefix = `switch_${switchComponent.id}`;
|
|
23
23
|
const name = this.componentName(snapshotArg.deviceConfig, `switch:${switchComponent.id}`, `Switch ${switchComponent.id}`);
|
|
24
|
-
features.push({ id: `${prefix}_output`, capability: 'switch', name, readable: true,
|
|
24
|
+
features.push({ id: `${prefix}_output`, capability: 'switch', name, readable: true,
|
|
25
|
+
writable: snapshotArg.controlEnabled && snapshotArg.methods.includes('Switch.Set') && snapshotArg.methods.includes('Switch.GetStatus') });
|
|
25
26
|
state.push({ featureId: `${prefix}_output`, value: switchComponent.status.output ?? null, updatedAt });
|
|
26
27
|
this.pushNumericFeature(features, state, `${prefix}_power`, `${name} power`, switchComponent.status.apower, 'W', updatedAt);
|
|
27
28
|
this.pushNumericFeature(features, state, `${prefix}_voltage`, `${name} voltage`, switchComponent.status.voltage, 'V', updatedAt);
|
|
@@ -36,7 +37,7 @@ export class ShellyMapper {
|
|
|
36
37
|
name: this.deviceName(snapshotArg.deviceInfo, snapshotArg.deviceConfig),
|
|
37
38
|
protocol: 'http',
|
|
38
39
|
manufacturer: 'Shelly',
|
|
39
|
-
model: snapshotArg.deviceInfo.model
|
|
40
|
+
model: snapshotArg.deviceInfo.model,
|
|
40
41
|
online: true,
|
|
41
42
|
features,
|
|
42
43
|
state,
|
|
@@ -46,20 +47,21 @@ export class ShellyMapper {
|
|
|
46
47
|
authEnabled: snapshotArg.deviceInfo.auth_en ?? snapshotArg.deviceInfo.auth,
|
|
47
48
|
macAddress: snapshotArg.deviceInfo.mac || snapshotArg.status.sys?.mac,
|
|
48
49
|
profile: snapshotArg.deviceInfo.profile,
|
|
50
|
+
homeId: snapshotArg.homeId,
|
|
49
51
|
},
|
|
50
52
|
}];
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
public static toEntities(snapshotArg: IShellySnapshot): IIntegrationEntity[] {
|
|
54
|
-
const deviceId = this.deviceId(snapshotArg
|
|
55
|
-
const deviceSlug =
|
|
56
|
+
const deviceId = this.deviceId(snapshotArg);
|
|
57
|
+
const deviceSlug = `${encodeURIComponent(snapshotArg.homeId)}.${encodeURIComponent(snapshotArg.deviceInfo.id!)}`;
|
|
56
58
|
const entities: IIntegrationEntity[] = [];
|
|
57
59
|
|
|
58
60
|
for (const switchComponent of this.switchComponents(snapshotArg.status)) {
|
|
59
61
|
const switchName = this.componentName(snapshotArg.deviceConfig, `switch:${switchComponent.id}`, `Switch ${switchComponent.id}`);
|
|
60
|
-
const uniquePrefix =
|
|
62
|
+
const uniquePrefix = `${deviceId}.switch_${switchComponent.id}`;
|
|
61
63
|
entities.push({
|
|
62
|
-
id: `switch.${deviceSlug}
|
|
64
|
+
id: `switch.${deviceSlug}.${switchComponent.id}`,
|
|
63
65
|
uniqueId: uniquePrefix,
|
|
64
66
|
integrationDomain: 'shelly',
|
|
65
67
|
deviceId,
|
|
@@ -72,11 +74,11 @@ export class ShellyMapper {
|
|
|
72
74
|
},
|
|
73
75
|
available: true,
|
|
74
76
|
});
|
|
75
|
-
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_power`, `sensor.${deviceSlug}
|
|
76
|
-
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_voltage`, `sensor.${deviceSlug}
|
|
77
|
-
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_current`, `sensor.${deviceSlug}
|
|
78
|
-
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_energy`, `sensor.${deviceSlug}
|
|
79
|
-
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_temperature`, `sensor.${deviceSlug}
|
|
77
|
+
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_power`, `sensor.${deviceSlug}.${switchComponent.id}.power`, `${switchName} power`, switchComponent.status.apower, 'W');
|
|
78
|
+
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_voltage`, `sensor.${deviceSlug}.${switchComponent.id}.voltage`, `${switchName} voltage`, switchComponent.status.voltage, 'V');
|
|
79
|
+
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_current`, `sensor.${deviceSlug}.${switchComponent.id}.current`, `${switchName} current`, switchComponent.status.current, 'A');
|
|
80
|
+
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_energy`, `sensor.${deviceSlug}.${switchComponent.id}.energy`, `${switchName} energy`, switchComponent.status.aenergy?.total, 'Wh');
|
|
81
|
+
this.pushSensorEntity(entities, deviceId, `${uniquePrefix}_temperature`, `sensor.${deviceSlug}.${switchComponent.id}.temperature`, `${switchName} temperature`, switchComponent.status.temperature?.tC, 'C');
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
return entities;
|
|
@@ -91,7 +93,7 @@ export class ShellyMapper {
|
|
|
91
93
|
unitArg: string,
|
|
92
94
|
updatedAtArg: string
|
|
93
95
|
): void {
|
|
94
|
-
if (typeof valueArg !== 'number') {
|
|
96
|
+
if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) {
|
|
95
97
|
return;
|
|
96
98
|
}
|
|
97
99
|
featuresArg.push({ id: idArg, capability: 'sensor', name: nameArg, readable: true, writable: false, unit: unitArg });
|
|
@@ -107,7 +109,7 @@ export class ShellyMapper {
|
|
|
107
109
|
valueArg: number | null | undefined,
|
|
108
110
|
unitArg: string
|
|
109
111
|
): void {
|
|
110
|
-
if (typeof valueArg !== 'number') {
|
|
112
|
+
if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) {
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
113
115
|
entitiesArg.push({
|
|
@@ -139,8 +141,9 @@ export class ShellyMapper {
|
|
|
139
141
|
return components.sort((leftArg, rightArg) => leftArg.id - rightArg.id);
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
public static deviceId(snapshotArg: IShellySnapshot): string {
|
|
145
|
+
if (!snapshotArg.homeId || !snapshotArg.deviceInfo.id) throw new TypeError('Shelly mapping requires admitted home and observed device identity.');
|
|
146
|
+
return `shelly.device.${encodeURIComponent(snapshotArg.homeId)}.${encodeURIComponent(snapshotArg.deviceInfo.id)}`;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
private static deviceName(deviceInfoArg: IShellyDeviceInfo, configArg?: IShellyDeviceConfig): string {
|
|
@@ -155,10 +158,6 @@ export class ShellyMapper {
|
|
|
155
158
|
return fallbackArg;
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
private static slug(valueArg: string): string {
|
|
159
|
-
return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'shelly';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
161
|
private static isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
|
|
163
162
|
return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
|
164
163
|
}
|
|
@@ -29,6 +29,8 @@ export const parseShellyDeviceInfo = (valueArg: unknown): IShellyDeviceInfo => {
|
|
|
29
29
|
if (valueArg.gen !== 2 && valueArg.gen !== 3) {
|
|
30
30
|
throw new DeviceCommunicationError('Shelly integration supports generation 2 and 3 devices.');
|
|
31
31
|
}
|
|
32
|
+
if (typeof valueArg.mac !== 'string' || !/^[0-9a-f]{12}$/i.test(valueArg.mac) ||
|
|
33
|
+
typeof valueArg.model !== 'string' || !valueArg.model || valueArg.model.length > 128) invalidPayload();
|
|
32
34
|
for (const key of ['mac', 'model', 'ver', 'fw_id', 'app', 'profile']) {
|
|
33
35
|
if (valueArg[key] !== undefined && typeof valueArg[key] !== 'string') invalidPayload();
|
|
34
36
|
}
|
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
export type TShellyProtocol = 'http' | 'https';
|
|
2
2
|
|
|
3
3
|
export interface IShellyConfig {
|
|
4
|
+
homeId: string;
|
|
4
5
|
host: string;
|
|
5
6
|
port?: number;
|
|
6
7
|
protocol?: TShellyProtocol;
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
expectedDeviceId: string;
|
|
9
|
+
expectedMacAddress: string;
|
|
10
|
+
expectedModel: string;
|
|
11
|
+
expectedGeneration: 2 | 3;
|
|
12
|
+
credentialRef?: string;
|
|
13
|
+
/** Absolute output controls are denied unless explicitly enabled. */
|
|
14
|
+
allowControl?: boolean;
|
|
15
|
+
allowUnencryptedLan?: boolean;
|
|
9
16
|
requestTimeoutMs?: number;
|
|
17
|
+
maxPendingOperations?: number;
|
|
18
|
+
maxResponseBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IShellyCredentialRequest {
|
|
22
|
+
readonly homeId: string;
|
|
23
|
+
readonly credentialRef: string;
|
|
24
|
+
readonly signal: AbortSignal;
|
|
25
|
+
/** Monotonic performance.now() deadline, including queue time. */
|
|
26
|
+
readonly deadline: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TShellyPasswordResolver = (requestArg: IShellyCredentialRequest) => Promise<string>;
|
|
30
|
+
|
|
31
|
+
export interface IShellyIntegrationOptions {
|
|
32
|
+
readonly homeId?: string;
|
|
33
|
+
readonly capacity?: number;
|
|
34
|
+
readonly resolvePassword?: TShellyPasswordResolver;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TShellyEffectOutcome = 'not-sent' | 'indeterminate';
|
|
38
|
+
|
|
39
|
+
export class ShellyOperationError extends Error {
|
|
40
|
+
constructor(messageArg: string, public readonly effectOutcome: TShellyEffectOutcome = 'not-sent',
|
|
41
|
+
public readonly commandAcknowledged = false) {
|
|
42
|
+
super(messageArg);
|
|
43
|
+
this.name = 'ShellyOperationError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IShellyEffectResult {
|
|
48
|
+
readonly componentId: number;
|
|
49
|
+
readonly requested: boolean;
|
|
50
|
+
readonly wasOn: boolean;
|
|
51
|
+
readonly commandAcknowledged: true;
|
|
52
|
+
readonly causalityConfirmed: false;
|
|
53
|
+
readonly outcome: 'observed-target' | 'indeterminate';
|
|
54
|
+
readonly snapshot: IShellySnapshot;
|
|
10
55
|
}
|
|
11
56
|
|
|
12
57
|
export interface IShellyDeviceInfo {
|
|
@@ -78,9 +123,12 @@ export interface IShellySwitchStatus {
|
|
|
78
123
|
}
|
|
79
124
|
|
|
80
125
|
export interface IShellySnapshot {
|
|
126
|
+
homeId: string;
|
|
81
127
|
deviceInfo: IShellyDeviceInfo;
|
|
82
128
|
status: IShellyStatus;
|
|
83
129
|
deviceConfig?: IShellyDeviceConfig;
|
|
130
|
+
methods: readonly string[];
|
|
131
|
+
controlEnabled: boolean;
|
|
84
132
|
}
|
|
85
133
|
|
|
86
134
|
export interface IShellyComponentEvent {
|