@smarthome.exchange/integrations 2.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +46 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/classes.nativeintegrationowner.d.ts +2 -0
- package/dist_ts/core/classes.nativeintegrationowner.js +5 -1
- package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
- package/dist_ts/core/classes.runtimemanager.js +11 -3
- package/dist_ts/core/types.d.ts +2 -0
- package/dist_ts/integrations/deconz/deconz.classes.client.d.ts +37 -29
- package/dist_ts/integrations/deconz/deconz.classes.client.js +521 -294
- package/dist_ts/integrations/deconz/deconz.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/deconz/deconz.classes.configflow.js +63 -33
- package/dist_ts/integrations/deconz/deconz.classes.integration.d.ts +4 -3
- package/dist_ts/integrations/deconz/deconz.classes.integration.js +85 -40
- package/dist_ts/integrations/deconz/deconz.mapper.d.ts +1 -0
- package/dist_ts/integrations/deconz/deconz.mapper.js +24 -6
- package/dist_ts/integrations/deconz/deconz.types.d.ts +43 -6
- package/dist_ts/integrations/deconz/deconz.validation.d.ts +7 -1
- package/dist_ts/integrations/deconz/deconz.validation.js +215 -4
- package/dist_ts/integrations/esphome/esphome.classes.client.d.ts +19 -7
- package/dist_ts/integrations/esphome/esphome.classes.client.js +183 -89
- package/dist_ts/integrations/esphome/esphome.classes.configflow.d.ts +5 -2
- package/dist_ts/integrations/esphome/esphome.classes.configflow.js +43 -33
- package/dist_ts/integrations/esphome/esphome.classes.integration.d.ts +8 -6
- package/dist_ts/integrations/esphome/esphome.classes.integration.js +71 -43
- package/dist_ts/integrations/esphome/esphome.discovery.js +4 -12
- package/dist_ts/integrations/esphome/esphome.mapper.d.ts +2 -2
- package/dist_ts/integrations/esphome/esphome.mapper.js +6 -4
- package/dist_ts/integrations/esphome/esphome.types.d.ts +31 -6
- package/dist_ts/integrations/esphome/esphome.types.js +1 -1
- package/dist_ts/integrations/esphome/esphome.validation.d.ts +7 -0
- package/dist_ts/integrations/esphome/esphome.validation.js +66 -0
- package/dist_ts/integrations/homeassistant/homeassistant.classes.client.d.ts +32 -4
- package/dist_ts/integrations/homeassistant/homeassistant.classes.client.js +345 -116
- package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.d.ts +8 -5
- package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.js +34 -8
- package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.d.ts +7 -4
- package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.js +44 -56
- package/dist_ts/integrations/homeassistant/homeassistant.types.d.ts +12 -1
- package/dist_ts/integrations/hue/hue.classes.client.d.ts +32 -4
- package/dist_ts/integrations/hue/hue.classes.client.js +331 -57
- package/dist_ts/integrations/hue/hue.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/hue/hue.classes.configflow.js +79 -27
- package/dist_ts/integrations/hue/hue.classes.integration.d.ts +3 -1
- package/dist_ts/integrations/hue/hue.classes.integration.js +83 -62
- package/dist_ts/integrations/hue/hue.mapper.d.ts +2 -0
- package/dist_ts/integrations/hue/hue.mapper.js +8 -6
- package/dist_ts/integrations/hue/hue.types.d.ts +32 -1
- package/dist_ts/integrations/hue/hue.validation.d.ts +9 -3
- package/dist_ts/integrations/hue/hue.validation.js +112 -11
- package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
- package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
- package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
- package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
- package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
- package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
- package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
- package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
- package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
- package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
- package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
- package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
- package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
- package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
- package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
- package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
- package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
- package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
- package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
- package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
- package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
- package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
- package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
- package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
- package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
- package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
- package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
- package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
- package/dist_ts/integrations/shelly/shelly.types.js +11 -2
- package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
- package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
- package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
- package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
- package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
- package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
- package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
- package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
- package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
- package/dist_ts/integrations/sonos/sonos.config.js +23 -2
- package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
- package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
- package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
- package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
- package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
- package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
- package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
- package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
- package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
- package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
- package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
- package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
- package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
- package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
- package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
- package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
- package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
- package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
- package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
- package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
- package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
- package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
- package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
- package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
- package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
- package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
- package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
- package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
- package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
- package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
- package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
- package/dist_ts/integrations/webostv/webostv.types.js +1 -1
- package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
- package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
- package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
- package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
- package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
- package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
- package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
- package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
- package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
- package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
- package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
- package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +10 -10
- package/readme.md +25 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/classes.nativeintegrationowner.ts +5 -0
- package/ts/core/classes.runtimemanager.ts +11 -2
- package/ts/core/types.ts +2 -0
- package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
- package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
- package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
- package/ts/integrations/deconz/deconz.mapper.ts +22 -5
- package/ts/integrations/deconz/deconz.types.ts +49 -6
- package/ts/integrations/deconz/deconz.validation.ts +177 -4
- package/ts/integrations/deconz/readme.md +15 -1
- package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
- package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
- package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
- package/ts/integrations/esphome/esphome.discovery.ts +3 -12
- package/ts/integrations/esphome/esphome.mapper.ts +9 -7
- package/ts/integrations/esphome/esphome.types.ts +35 -6
- package/ts/integrations/esphome/esphome.validation.ts +66 -0
- package/ts/integrations/esphome/readme.md +20 -6
- package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
- package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
- package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
- package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
- package/ts/integrations/homeassistant/readme.md +22 -6
- package/ts/integrations/hue/hue.classes.client.ts +288 -55
- package/ts/integrations/hue/hue.classes.configflow.ts +68 -25
- package/ts/integrations/hue/hue.classes.integration.ts +75 -54
- package/ts/integrations/hue/hue.mapper.ts +8 -5
- package/ts/integrations/hue/hue.types.ts +37 -1
- package/ts/integrations/hue/hue.validation.ts +102 -13
- package/ts/integrations/hue/readme.md +49 -12
- package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
- package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
- package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
- package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
- package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
- package/ts/integrations/mqtt/mqtt.types.ts +52 -0
- package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
- package/ts/integrations/mqtt/readme.md +12 -3
- package/ts/integrations/samsungtv/readme.md +11 -5
- package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
- package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
- package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
- package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
- package/ts/integrations/shelly/readme.md +22 -7
- package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
- package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
- package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
- package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
- package/ts/integrations/shelly/shelly.mapper.ts +19 -20
- package/ts/integrations/shelly/shelly.rpc.ts +2 -0
- package/ts/integrations/shelly/shelly.types.ts +50 -2
- package/ts/integrations/shelly/shelly.validation.ts +76 -0
- package/ts/integrations/sonos/readme.md +117 -38
- package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
- package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
- package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
- package/ts/integrations/sonos/sonos.config.ts +24 -2
- package/ts/integrations/sonos/sonos.mapper.ts +13 -8
- package/ts/integrations/sonos/sonos.matchers.ts +2 -2
- package/ts/integrations/sonos/sonos.types.ts +30 -1
- package/ts/integrations/tasmota/readme.md +16 -5
- package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
- package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
- package/ts/integrations/tasmota/tasmota.config.ts +17 -0
- package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
- package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
- package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
- package/ts/integrations/tasmota/tasmota.types.ts +43 -2
- package/ts/integrations/tuya/readme.md +52 -10
- package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
- package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
- package/ts/integrations/tuya/tuya.mapper.ts +1 -1
- package/ts/integrations/tuya/tuya.types.ts +4 -0
- package/ts/integrations/tuya/tuya.validation.ts +5 -8
- package/ts/integrations/webostv/readme.md +115 -22
- package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
- package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
- package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
- package/ts/integrations/webostv/webostv.mapper.ts +4 -4
- package/ts/integrations/webostv/webostv.types.ts +18 -3
- package/ts/integrations/webostv/webostv.validation.ts +22 -1
- package/ts/integrations/zigbee2mqtt/readme.md +22 -27
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
- package/ts/integrations/zwave_js/readme.md +42 -9
- package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
- package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
- package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
- package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
- package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
- package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
- package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
- package/ts/plugins.ts +2 -1
|
@@ -1,40 +1,35 @@
|
|
|
1
|
-
# Zigbee2MQTT
|
|
1
|
+
# Native Zigbee2MQTT
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The adapter connects to an explicitly selected MQTT broker and Zigbee2MQTT base topic through the existing native MQTT.js transport. SHX home selection is separate from the Zigbee coordinator, network, IEEE address and friendly name. Broker TLS authenticates the selected broker name; Zigbee identity/inventory observations do not prove a unique or cryptographically authenticated physical controller.
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
|
-
const integration = new Zigbee2MqttIntegration(
|
|
6
|
+
const integration = new Zigbee2MqttIntegration({
|
|
7
|
+
homeId: hostSelectedHomeId,
|
|
8
|
+
controls: [{ ieeeAddress: selectedLowercaseIeeeAddress, property: 'state' }],
|
|
9
|
+
resolveCredentials: async (request) => {
|
|
10
|
+
// The host resolver owns storage. It validates the admitted home and exact
|
|
11
|
+
// request.host/port/protocol/baseTopic before returning runtime-only material.
|
|
12
|
+
return retrieveSelectedBrokerCredentials(request);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
7
15
|
const runtime = await integration.setup({
|
|
8
|
-
|
|
9
|
-
protocol: 'mqtts',
|
|
10
|
-
port: 8883,
|
|
16
|
+
homeId: hostSelectedHomeId,
|
|
17
|
+
host: 'broker.internal', protocol: 'mqtts', port: 8883,
|
|
11
18
|
baseTopic: 'home/zigbee2mqtt',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}, { abortSignal: lifetimeController.signal });
|
|
15
|
-
|
|
16
|
-
const [device] = await runtime.devices();
|
|
17
|
-
const power = device?.features.find((feature) => feature.id === 'state');
|
|
18
|
-
if (device?.online && power?.writable) {
|
|
19
|
-
await runtime.writeFeature?.({ device, feature: power, value: true });
|
|
20
|
-
}
|
|
21
|
-
await runtime.destroy();
|
|
19
|
+
authentication: 'provider', credentialKey: 'broker-reference',
|
|
20
|
+
}, { homeId: hostSelectedHomeId, abortSignal: hostLifetime.signal });
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Every setup and config-flow context requires an independently selected `homeId`; an optional constructor binding does not replace it. Config, form fields, discovery, store scope and network IDs cannot choose home. One instance binds once even if setup later fails. Canonical device IDs are `encodeURIComponent(homeId) + ':zigbee2mqtt.' + lowercaseIeeeAddress`; entity IDs append the exact exposed property. Close is terminal.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
`authentication` is required and explicitly selects `anonymous`, `provider` or `config-store`. Anonymous forbids a credential reference. Provider requires both an existing `credentialKey` and constructor-injected `resolveCredentials`; missing provider does not fall back to another source. Config-store requires a borrowed actual scoped `NoSqlConfigStore` and the reference. It reads an encrypted value shaped as `{ version: 1, host, port, protocol, baseTopic, credentials: { username?, password?, ca?, cert?, key?, servername? } }` and verifies exact selected broker/topic binding before use. Configuration and flow output contain references only; passwords, CA/client certificates and private keys are runtime-only strings. No filesystem credential paths are supported. Client cert/key must be supplied together, TLS material requires mqtts, and certificate/hostname verification remains enabled. Plain MQTT requires `allowUnencryptedLan: true`. No-store anonymous and provider paths are legitimate.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
The resolver request includes captured home, reference, exact broker/topic, an AbortSignal and an absolute performance.now() setup deadline. Provider work is owned before invocation. Deadline/abort prevents late transport allocation; destruction joins the actual foreign promise even if it ignores cancellation. Providers and borrowed store/database/key-provider resources remain caller-owned. Cleanup retry never reruns a provider, setup, subscription or physical command.
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
Primitive binary, numeric, enum and text controls are preserved, including advertised primitive children of light/switch/fan/cover/lock/climate. Constructor `controls` is an immutable finite exact lowercase IEEE/property allowlist. Default is no control permission. Expose write access alone does not grant owner permission; editable config cannot grant it. At dispatch, current canonical home/target/capability, expose type/access/range/step/enum, supported/non-disabled membership and authoritative availability must all agree. Complex colors, lists, schedules, arbitrary ZCL, groups, joining, pairing, binding, OTA, bridge configuration and /get are excluded.
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
Each admitted write publishes exactly one property to BASE/IEEE/set with QoS0 and retain:false. Commands are never retried or optimistically applied. Success preserves `{ confirmation: 'mqtt-publish', physicalOutcome: 'unconfirmed' }`; publication callback loss reports uncertainty. Device state changes only from subsequent observations. Retained and pre-inventory action reports never replay physical events. Current-generation cache seeding and callbacks preserve fresh inventory/availability across reconnects; a stale historical snapshot cannot admit control.
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
The owner shares a default capacity of 16 across pending setup/flow work, live runtimes and retained cleanup failures. Existing device/payload/buffer/deadline/write/subscriber bounds remain. Async foreign subscriber work is additionally bounded at 256 pending callbacks and drained during closure. A failed subscriber is removed and other subscribers receive a sanitized error, preserving established isolation behavior. Integration `destroy()` drains flow/setup/runtime, actual providers, subscriber work, commands and transport; repeated/reentrant calls share a stable promise. `retryCleanup()` retries retained failed cleanup only. Runtime destruction closes its lease. Config-flow destruction is terminal for its scope.
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Groups, pairing, joining, binding, OTA, bridge configuration, network management, `/get` polling and Home Assistant discovery are outside this adapter. Tests use the real MQTT.js TCP parser against a local test broker; no hardware access is performed.
|
|
39
|
-
|
|
40
|
-
Protocol references: [MQTT topics and messages](https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html), [exposes and access masks](https://www.zigbee2mqtt.io/guide/usage/exposes.html), and [device availability](https://www.zigbee2mqtt.io/guide/configuration/device-availability.html).
|
|
35
|
+
Verification uses real local MQTT.js TCP/TLS fixtures, including private CA/mutual client authentication and wrong broker-name denial. No hardware/private production broker acceptance has been established.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
|
|
2
2
|
import type { IZigbee2MqttConfig } from './zigbee2mqtt.types.js';
|
|
3
|
-
import { zigbee2MqttConfig } from './zigbee2mqtt.validation.js';
|
|
3
|
+
import { zigbee2MqttConfig, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
|
|
4
|
+
import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
|
|
4
5
|
|
|
5
6
|
export class Zigbee2MqttConfigFlow implements IConfigFlow<IZigbee2MqttConfig> {
|
|
6
|
-
|
|
7
|
+
readonly #scope: NativeIntegrationScope;
|
|
8
|
+
constructor(ownerArg = new NativeIntegrationOwner('zigbee2mqtt')) { this.#scope = ownerArg.createFlowScope(); }
|
|
9
|
+
public async start(_candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IZigbee2MqttConfig>> {
|
|
10
|
+
const context = this.#scope.admit(contextArg);
|
|
7
11
|
return {
|
|
8
12
|
kind: 'form', title: 'Connect Zigbee2MQTT',
|
|
9
13
|
description: 'Enter the broker and exact Zigbee2MQTT base topic. Enable device availability in Zigbee2MQTT before operating devices.',
|
|
@@ -12,15 +16,20 @@ export class Zigbee2MqttConfigFlow implements IConfigFlow<IZigbee2MqttConfig> {
|
|
|
12
16
|
{ name: 'baseTopic', label: 'Zigbee2MQTT base topic', type: 'text', required: true },
|
|
13
17
|
{ name: 'port', label: 'Broker port', type: 'number' },
|
|
14
18
|
{ name: 'protocol', label: 'Protocol', type: 'select', options: [{ label: 'MQTT', value: 'mqtt' }, { label: 'MQTTS', value: 'mqtts' }] },
|
|
15
|
-
{ name: '
|
|
16
|
-
{ name: '
|
|
19
|
+
{ name: 'authentication', label: 'Credential source', type: 'select', required: true, options: [{ label: 'Anonymous', value: 'anonymous' }, { label: 'Caller provider', value: 'provider' }, { label: 'Encrypted config store', value: 'config-store' }] },
|
|
20
|
+
{ name: 'credentialKey', label: 'Existing credential reference', type: 'text' },
|
|
21
|
+
{ name: 'allowUnencryptedLan', label: 'Allow plaintext LAN MQTT', type: 'boolean' },
|
|
17
22
|
],
|
|
18
|
-
submit: async (
|
|
23
|
+
submit: (valuesArg) => this.#scope.run(context, undefined, async () => {
|
|
19
24
|
try {
|
|
20
|
-
const
|
|
25
|
+
const values = zigbee2MqttRecord(valuesArg);
|
|
26
|
+
if (!values || values.homeId !== undefined && values.homeId !== context.homeId) throw new Error('Conflicting home or invalid form data.');
|
|
27
|
+
const config = zigbee2MqttConfig({ ...values, homeId: context.homeId } as unknown as IZigbee2MqttConfig);
|
|
21
28
|
return { kind: 'done', title: 'Zigbee2MQTT configured', config };
|
|
22
29
|
} catch { return { kind: 'error', error: 'Enter a valid broker, base topic and supported explicit configuration fields.' }; }
|
|
23
|
-
},
|
|
30
|
+
}),
|
|
24
31
|
};
|
|
25
32
|
}
|
|
33
|
+
public destroy(): Promise<void> { return this.#scope.close(); }
|
|
34
|
+
public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
|
|
26
35
|
}
|
|
@@ -1,23 +1,70 @@
|
|
|
1
1
|
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
|
2
2
|
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
3
|
+
import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
|
|
3
4
|
import type { IIntegrationRuntime, IIntegrationSetupContext } from '../../core/types.js';
|
|
4
5
|
import { Zigbee2MqttConfigFlow } from './zigbee2mqtt.classes.configflow.js';
|
|
5
6
|
import { Zigbee2MqttRuntime } from './zigbee2mqtt.classes.runtime.js';
|
|
6
|
-
import
|
|
7
|
+
import { MqttSession } from '../mqtt/mqtt.classes.session.js';
|
|
8
|
+
import type { TMqttCredentialResolver } from '../mqtt/mqtt.types.js';
|
|
9
|
+
import type { IZigbee2MqttConfig, IZigbee2MqttIntegrationOptions } from './zigbee2mqtt.types.js';
|
|
10
|
+
import { zigbee2MqttConfig, zigbee2MqttCredentials, zigbee2MqttOptions, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
|
|
7
11
|
|
|
8
12
|
export class Zigbee2MqttIntegration extends BaseIntegration<IZigbee2MqttConfig> {
|
|
9
13
|
public readonly domain = 'zigbee2mqtt';
|
|
10
14
|
public readonly displayName = 'Zigbee2MQTT';
|
|
11
15
|
public readonly status = 'control-runtime' as const;
|
|
12
16
|
public readonly discoveryDescriptor = new DiscoveryDescriptor({ integrationDomain: this.domain, displayName: this.displayName });
|
|
13
|
-
public readonly configFlow
|
|
17
|
+
public readonly configFlow: Zigbee2MqttConfigFlow;
|
|
18
|
+
readonly #owner: NativeIntegrationOwner;
|
|
19
|
+
readonly #options: IZigbee2MqttIntegrationOptions;
|
|
20
|
+
readonly #controls: ReadonlySet<string>;
|
|
14
21
|
public readonly metadata = { source: 'zigbee2mqtt', upstreamPath: 'Koenkk/zigbee2mqtt', iotClass: 'local_push' };
|
|
15
22
|
|
|
23
|
+
constructor(optionsArg: IZigbee2MqttIntegrationOptions = {}) {
|
|
24
|
+
super();
|
|
25
|
+
const { options, controls } = zigbee2MqttOptions(optionsArg);
|
|
26
|
+
this.#options = options;
|
|
27
|
+
this.#controls = controls;
|
|
28
|
+
this.#owner = new NativeIntegrationOwner(this.domain, { homeId: options.homeId, capacity: options.capacity });
|
|
29
|
+
this.configFlow = new Zigbee2MqttConfigFlow(this.#owner);
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
public async setup(configArg: IZigbee2MqttConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
const config = zigbee2MqttConfig(configArg);
|
|
34
|
+
return this.#owner.runSetup(contextArg, config.homeId, async (lease) => {
|
|
35
|
+
let resolver: TMqttCredentialResolver | undefined;
|
|
36
|
+
if (config.authentication === 'provider') {
|
|
37
|
+
const provider = this.#options.resolveCredentials;
|
|
38
|
+
if (!provider) throw new Error('Zigbee2MQTT provider mode requires an explicit caller resolver.');
|
|
39
|
+
resolver = async (request) => zigbee2MqttCredentials(config, await provider(Object.freeze({ ...request, host: config.host, port: config.port!, protocol: config.protocol!, baseTopic: config.baseTopic })));
|
|
40
|
+
} else if (config.authentication === 'config-store') {
|
|
41
|
+
const store = lease.context.configStore;
|
|
42
|
+
if (!store) throw new Error('Zigbee2MQTT config-store mode requires an admitted encrypted NoSqlConfigStore.');
|
|
43
|
+
resolver = async (request) => {
|
|
44
|
+
const value = zigbee2MqttRecord(await store.get<unknown>(request.credentialKey));
|
|
45
|
+
if (!value || Object.keys(value).some((key) => !['version', 'host', 'port', 'protocol', 'baseTopic', 'credentials'].includes(key)) ||
|
|
46
|
+
value.version !== 1 || value.host !== config.host || value.port !== config.port || value.protocol !== config.protocol || value.baseTopic !== config.baseTopic) {
|
|
47
|
+
throw new Error('Zigbee2MQTT encrypted credentials do not match the selected broker and base topic.');
|
|
48
|
+
}
|
|
49
|
+
return zigbee2MqttCredentials(config, value.credentials);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const session = new MqttSession({ ...config, discovery: false, birthMessage: false, willMessage: false, clean: true,
|
|
53
|
+
subscriptions: [config.baseTopic + '/#'], maxMessageTopics: (config.maxDevices ?? 2048) * 4 + 16 }, lease, resolver);
|
|
54
|
+
let runtime: Zigbee2MqttRuntime | undefined;
|
|
55
|
+
let attempts = 0;
|
|
56
|
+
const closeSession = (): Promise<void> => attempts++ === 0 ? session.close() : session.retryCleanup();
|
|
57
|
+
lease.own(() => runtime ? runtime.cleanup() : closeSession());
|
|
58
|
+
const client = await session.start();
|
|
59
|
+
lease.assertOpen();
|
|
60
|
+
runtime = new Zigbee2MqttRuntime(config, client, lease, closeSession, this.#controls);
|
|
61
|
+
await runtime.start();
|
|
62
|
+
lease.assertOpen();
|
|
63
|
+
lease.promoteToRuntime();
|
|
64
|
+
return runtime;
|
|
65
|
+
});
|
|
20
66
|
}
|
|
21
67
|
|
|
22
|
-
public
|
|
68
|
+
public destroy(): Promise<void> { return this.#owner.close(); }
|
|
69
|
+
public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
|
|
23
70
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
|
|
3
|
-
import { MqttClient } from '../mqtt/mqtt.classes.client.js';
|
|
3
|
+
import type { MqttClient } from '../mqtt/mqtt.classes.client.js';
|
|
4
4
|
import type { IMqttMessage } from '../mqtt/mqtt.types.js';
|
|
5
5
|
import { Zigbee2MqttMapper } from './zigbee2mqtt.mapper.js';
|
|
6
6
|
import type { IZigbee2MqttConfig, IZigbee2MqttDevice, IZigbee2MqttReportedState, IZigbee2MqttSnapshot } from './zigbee2mqtt.types.js';
|
|
7
|
-
import {
|
|
7
|
+
import { zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
|
|
8
|
+
import type { INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
|
|
8
9
|
|
|
9
10
|
interface IZigbee2MqttListener {
|
|
10
11
|
handler: (eventArg: IIntegrationEvent) => void;
|
|
@@ -14,7 +15,7 @@ interface IZigbee2MqttListener {
|
|
|
14
15
|
export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
15
16
|
public readonly domain = 'zigbee2mqtt';
|
|
16
17
|
private readonly config: IZigbee2MqttConfig;
|
|
17
|
-
|
|
18
|
+
readonly #controls: ReadonlySet<string>;
|
|
18
19
|
private readonly removeMessageHandler: () => void;
|
|
19
20
|
private readonly removeConnectionHandler: () => void;
|
|
20
21
|
private readonly handlers = new Set<IZigbee2MqttListener>();
|
|
@@ -34,17 +35,18 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
34
35
|
private inventoryCurrent = false;
|
|
35
36
|
private inventoryError?: Error;
|
|
36
37
|
private destroyed = false;
|
|
37
|
-
private destroyPromise?: Promise<void>;
|
|
38
38
|
private activeWrites = 0;
|
|
39
|
-
private
|
|
39
|
+
private readonly pendingHandlers = new Set<Promise<void>>();
|
|
40
|
+
private readonly callbackFailures = new Set<Error>();
|
|
41
|
+
private readonly ownerAbort = (): void => { this.destroyed = true; };
|
|
40
42
|
|
|
41
|
-
constructor(configArg: IZigbee2MqttConfig
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
43
|
+
constructor(configArg: IZigbee2MqttConfig, private readonly client: MqttClient,
|
|
44
|
+
private readonly lease: INativeSetupLease, private readonly closeSession: () => Promise<void>,
|
|
45
|
+
controlsArg: ReadonlySet<string>) {
|
|
46
|
+
this.config = configArg;
|
|
47
|
+
this.#controls = new Set(controlsArg);
|
|
48
|
+
this.connected = client.connected;
|
|
49
|
+
lease.signal.addEventListener('abort', this.ownerAbort, { once: true });
|
|
48
50
|
this.removeMessageHandler = this.client.onMessage((messageArg) => this.receive(messageArg));
|
|
49
51
|
this.removeConnectionHandler = this.client.onConnectionState((stateArg) => {
|
|
50
52
|
const previous = this.snapshot();
|
|
@@ -64,24 +66,16 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
public async start(
|
|
68
|
-
this.assertOpen(
|
|
69
|
+
public async start(): Promise<void> {
|
|
70
|
+
this.assertOpen();
|
|
69
71
|
if (this.started) throw new Error('Zigbee2MQTT runtime has already started.');
|
|
70
72
|
this.started = true;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
await this.client.start(abortSignalArg);
|
|
78
|
-
this.assertOpen(abortSignalArg);
|
|
79
|
-
await this.waitForInventory(abortSignalArg);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
try { await this.destroy(); }
|
|
82
|
-
catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Zigbee2MQTT setup and cleanup failed.'); }
|
|
83
|
-
throw error;
|
|
73
|
+
// Handlers are installed before this synchronous current-generation snapshot. No await
|
|
74
|
+
// can let older cached observations overwrite a later callback or a reconnect.
|
|
75
|
+
for (const message of this.client.getCurrentSnapshot()) {
|
|
76
|
+
if (message.topic !== this.config.baseTopic + '/bridge/event') this.receive(message, true);
|
|
84
77
|
}
|
|
78
|
+
await this.waitForInventory(this.lease.signal);
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
public isHealthy(): boolean {
|
|
@@ -128,6 +122,9 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
128
122
|
!device || device.integrationDomain !== this.domain || typeof device.id !== 'string' || !feature || typeof feature.id !== 'string') {
|
|
129
123
|
throw new Error('Zigbee2MQTT feature request requires a device owned by this domain.');
|
|
130
124
|
}
|
|
125
|
+
const selected = this.findDevice(device.id);
|
|
126
|
+
const current = selected?.exposes.find((expose) => expose.property === feature.id);
|
|
127
|
+
if (!current || feature.capability !== current.capability) throw new Error('Zigbee2MQTT feature capability does not match the current target.');
|
|
131
128
|
return await this.write(device.id, feature.id, request.value, abortSignalArg);
|
|
132
129
|
} catch (error) { return this.failure(error); }
|
|
133
130
|
}
|
|
@@ -149,26 +146,23 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
149
146
|
selected = this.findDevice(target.entityId.slice(0, -(data.property.length + 1)));
|
|
150
147
|
}
|
|
151
148
|
if (!selected) throw new Error('Zigbee2MQTT target is not in the current inventory.');
|
|
152
|
-
return await this.write(Zigbee2MqttMapper.deviceId(selected.ieeeAddress), data.property, data.value);
|
|
149
|
+
return await this.write(Zigbee2MqttMapper.deviceId(this.config.homeId, selected.ieeeAddress), data.property, data.value);
|
|
153
150
|
} catch (error) { return this.failure(error); }
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
public destroy(): Promise<void> {
|
|
157
|
-
if (!this.destroyPromise) this.destroyPromise = this.destroyConnection();
|
|
158
|
-
return this.destroyPromise;
|
|
159
|
-
}
|
|
153
|
+
public destroy(): Promise<void> { return this.lease.close(); }
|
|
160
154
|
|
|
161
155
|
private async write(deviceIdArg: string, propertyArg: string, valueArg: unknown, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
|
|
162
156
|
this.assertOpen(abortSignalArg);
|
|
163
157
|
const device = this.findDevice(deviceIdArg);
|
|
164
158
|
const expose = device?.exposes.find((exposeArg) => exposeArg.property === propertyArg);
|
|
165
|
-
if (!device || !expose || !Zigbee2MqttMapper.available(this.snapshot([device]), device)) throw new Error('Zigbee2MQTT target is unavailable or not exposed.');
|
|
159
|
+
if (!device || !expose || !Zigbee2MqttMapper.writable(this.snapshot([device]), device, expose) || !Zigbee2MqttMapper.available(this.snapshot([device]), device)) throw new Error('Zigbee2MQTT target is unavailable or not exposed.');
|
|
166
160
|
const value = Zigbee2MqttMapper.commandValue(expose, valueArg);
|
|
167
161
|
if (value === undefined) throw new Error('Zigbee2MQTT value is not permitted by the advertised expose.');
|
|
168
162
|
if (this.activeWrites >= 64) throw new Error('Zigbee2MQTT command admission limit exceeded.');
|
|
169
163
|
this.activeWrites++;
|
|
170
164
|
try {
|
|
171
|
-
const publication = this.client.publish({ topic: `${this.config.baseTopic}/${device.ieeeAddress}/set`, payload: { [expose.property]: value }, qos: 0, retain: false }, abortSignalArg);
|
|
165
|
+
const publication = this.client.publish({ topic: `${this.config.baseTopic}/${device.ieeeAddress}/set`, payload: { [expose.property]: value }, qos: 0, retain: false }, abortSignalArg ? AbortSignal.any([this.lease.signal, abortSignalArg]) : this.lease.signal);
|
|
172
166
|
this.pendingWrites.add(publication);
|
|
173
167
|
try { await publication; }
|
|
174
168
|
catch { throw new Error('Zigbee2MQTT command publication failed; its physical outcome is unconfirmed.'); }
|
|
@@ -297,8 +291,8 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
297
291
|
if (value === undefined) { states.delete(expose.property); this.emitError('Zigbee2MQTT reported a value outside its expose type.'); continue; }
|
|
298
292
|
states.set(expose.property, { value, updatedAt: new Date(timestampArg).toISOString() });
|
|
299
293
|
if (liveEventArg && (expose.property === 'action' || expose.property.startsWith('action_'))) {
|
|
300
|
-
this.emit({ type: 'state_changed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(deviceArg.ieeeAddress),
|
|
301
|
-
entityId: Zigbee2MqttMapper.entityId(deviceArg.ieeeAddress, expose.property), data: { value }, timestamp: timestampArg });
|
|
294
|
+
this.emit({ type: 'state_changed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(this.config.homeId, deviceArg.ieeeAddress),
|
|
295
|
+
entityId: Zigbee2MqttMapper.entityId(this.config.homeId, deviceArg.ieeeAddress, expose.property), data: { value }, timestamp: timestampArg });
|
|
302
296
|
}
|
|
303
297
|
}
|
|
304
298
|
}
|
|
@@ -307,10 +301,10 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
307
301
|
const next = this.snapshot(scopedDevicesArg);
|
|
308
302
|
const previousDevices = new Map(previousArg.devices.map((deviceArg) => [deviceArg.ieeeAddress, deviceArg]));
|
|
309
303
|
for (const device of next.devices) {
|
|
310
|
-
if (!previousDevices.has(device.ieeeAddress)) this.emit({ type: 'device_added', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(device.ieeeAddress), timestamp: Date.now() });
|
|
304
|
+
if (!previousDevices.has(device.ieeeAddress)) this.emit({ type: 'device_added', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(this.config.homeId, device.ieeeAddress), timestamp: Date.now() });
|
|
311
305
|
}
|
|
312
306
|
for (const device of previousArg.devices) {
|
|
313
|
-
if (!this.devicesByIeee.has(device.ieeeAddress)) this.emit({ type: 'device_removed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(device.ieeeAddress), timestamp: Date.now() });
|
|
307
|
+
if (!this.devicesByIeee.has(device.ieeeAddress)) this.emit({ type: 'device_removed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(this.config.homeId, device.ieeeAddress), timestamp: Date.now() });
|
|
314
308
|
}
|
|
315
309
|
const previous = new Map(Zigbee2MqttMapper.toEntities(previousArg).map((entityArg) => [entityArg.id, entityArg]));
|
|
316
310
|
const current = new Map(Zigbee2MqttMapper.toEntities(next).map((entityArg) => [entityArg.id, entityArg]));
|
|
@@ -331,7 +325,7 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
331
325
|
private snapshot(scopedDevicesArg?: IZigbee2MqttDevice[]): IZigbee2MqttSnapshot {
|
|
332
326
|
const devices = scopedDevicesArg ?? [...this.devicesByIeee.values()];
|
|
333
327
|
return {
|
|
334
|
-
devices, connected: this.connected && !this.destroyed,
|
|
328
|
+
homeId: this.config.homeId, controls: this.#controls, devices, connected: this.connected && !this.destroyed,
|
|
335
329
|
bridgeOnline: this.bridgeOnline, inventoryCurrent: this.inventoryCurrent,
|
|
336
330
|
availability: new Map(devices.filter((deviceArg) => this.availability.has(deviceArg.ieeeAddress)).map((deviceArg) =>
|
|
337
331
|
[deviceArg.ieeeAddress, this.availability.get(deviceArg.ieeeAddress) === true])),
|
|
@@ -340,7 +334,8 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
340
334
|
}
|
|
341
335
|
|
|
342
336
|
private findDevice(idArg: string): IZigbee2MqttDevice | undefined {
|
|
343
|
-
|
|
337
|
+
const prefix = `${encodeURIComponent(this.config.homeId)}:zigbee2mqtt.`;
|
|
338
|
+
return idArg.startsWith(prefix) ? this.devicesByIeee.get(idArg.slice(prefix.length)) : undefined;
|
|
344
339
|
}
|
|
345
340
|
|
|
346
341
|
private deviceForTopic(topicArg: string): IZigbee2MqttDevice | undefined {
|
|
@@ -370,37 +365,55 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
|
|
|
370
365
|
if (this.inventoryReceived && this.bridgeReceived && this.client.connected) for (const waiter of [...this.readyWaiters]) waiter.resolve();
|
|
371
366
|
}
|
|
372
367
|
|
|
373
|
-
|
|
368
|
+
public async cleanup(): Promise<void> {
|
|
374
369
|
this.destroyed = true;
|
|
375
|
-
this.
|
|
370
|
+
this.lease.signal.removeEventListener('abort', this.ownerAbort);
|
|
376
371
|
this.removeMessageHandler();
|
|
377
372
|
this.removeConnectionHandler();
|
|
378
373
|
for (const waiter of [...this.readyWaiters]) waiter.reject(new Error('Zigbee2MQTT runtime closed.'));
|
|
379
|
-
|
|
374
|
+
const cleanups = [...this.subscriptionCleanups];
|
|
380
375
|
this.handlers.clear();
|
|
381
|
-
await
|
|
382
|
-
|
|
376
|
+
const results = await Promise.allSettled([
|
|
377
|
+
...cleanups.map((cleanup) => Promise.resolve().then(cleanup)),
|
|
378
|
+
Promise.resolve().then(this.closeSession),
|
|
379
|
+
...[...this.pendingWrites].map((write) => write.then(() => undefined, () => undefined)),
|
|
380
|
+
...this.pendingHandlers,
|
|
381
|
+
]);
|
|
383
382
|
this.devicesByIeee.clear();
|
|
384
383
|
this.devicesByName.clear();
|
|
385
384
|
this.availability.clear();
|
|
386
385
|
this.state.clear();
|
|
387
386
|
this.pendingMessages.clear();
|
|
387
|
+
const failures = [...this.callbackFailures, ...results.flatMap((result) => result.status === 'rejected' ? [result.reason as unknown] : [])];
|
|
388
|
+
this.callbackFailures.clear();
|
|
389
|
+
if (failures.length) throw new AggregateError(failures, 'Zigbee2MQTT runtime cleanup failed; retry cleanup explicitly.');
|
|
388
390
|
}
|
|
389
391
|
|
|
390
392
|
private assertOpen(abortSignalArg?: AbortSignal): void {
|
|
391
393
|
if (abortSignalArg?.aborted) throw new DOMException('Zigbee2MQTT operation aborted.', 'AbortError');
|
|
392
394
|
if (this.destroyed) throw new Error('Zigbee2MQTT runtime closed.');
|
|
395
|
+
this.lease.assertOpen();
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
private emit(eventArg: IIntegrationEvent): void {
|
|
396
399
|
for (const listener of [...this.handlers]) {
|
|
397
400
|
if (!this.handlers.has(listener)) continue;
|
|
398
|
-
|
|
399
|
-
catch {
|
|
400
|
-
// A consumer callback must not disconnect the transport shared by other subscribers.
|
|
401
|
+
if (this.pendingHandlers.size >= 256) {
|
|
401
402
|
void listener.cleanup();
|
|
402
|
-
this.emitError('Zigbee2MQTT subscriber
|
|
403
|
+
this.emitError('Zigbee2MQTT subscriber work limit exceeded.');
|
|
404
|
+
continue;
|
|
403
405
|
}
|
|
406
|
+
const work = Promise.resolve().then(async () => {
|
|
407
|
+
if (!this.handlers.has(listener) || this.destroyed) return;
|
|
408
|
+
try { await listener.handler(structuredClone(eventArg)); }
|
|
409
|
+
catch {
|
|
410
|
+
await listener.cleanup();
|
|
411
|
+
if (this.destroyed) this.callbackFailures.add(new Error('Zigbee2MQTT subscriber callback failed while draining.'));
|
|
412
|
+
else this.emitError('Zigbee2MQTT subscriber callback failed.');
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
this.pendingHandlers.add(work);
|
|
416
|
+
void work.then(() => this.pendingHandlers.delete(work), () => this.pendingHandlers.delete(work));
|
|
404
417
|
}
|
|
405
418
|
}
|
|
406
419
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import type * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IIntegrationEntity } from '../../core/types.js';
|
|
3
3
|
import type { IZigbee2MqttDevice, IZigbee2MqttExpose, IZigbee2MqttSnapshot, TZigbee2MqttPrimitive } from './zigbee2mqtt.types.js';
|
|
4
|
-
import { zigbee2MqttPrimitive, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
|
|
4
|
+
import { zigbee2MqttControlKey, zigbee2MqttPrimitive, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
|
|
5
5
|
|
|
6
6
|
const specificTypes = new Set(['light', 'switch', 'fan', 'cover', 'lock', 'climate']);
|
|
7
7
|
const forbiddenProperties = new Set(['__proto__', 'constructor', 'prototype', 'read', 'write', 'command', 'commands', 'get', 'set']);
|
|
8
8
|
|
|
9
9
|
export class Zigbee2MqttMapper {
|
|
10
|
-
public static deviceId(ieeeAddressArg: string): string {
|
|
11
|
-
return
|
|
10
|
+
public static deviceId(homeIdArg: string, ieeeAddressArg: string): string {
|
|
11
|
+
return `${encodeURIComponent(homeIdArg)}:zigbee2mqtt.${ieeeAddressArg}`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
public static entityId(ieeeAddressArg: string, propertyArg: string): string {
|
|
15
|
-
return `${this.deviceId(ieeeAddressArg)}.${propertyArg}`;
|
|
14
|
+
public static entityId(homeIdArg: string, ieeeAddressArg: string, propertyArg: string): string {
|
|
15
|
+
return `${this.deviceId(homeIdArg, ieeeAddressArg)}.${propertyArg}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
public static parseInventory(payloadArg: unknown, maxDevicesArg: number): IZigbee2MqttDevice[] {
|
|
@@ -54,12 +54,12 @@ export class Zigbee2MqttMapper {
|
|
|
54
54
|
|
|
55
55
|
public static toDevices(snapshotArg: IZigbee2MqttSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
|
|
56
56
|
return snapshotArg.devices.map((deviceArg) => ({
|
|
57
|
-
id: this.deviceId(deviceArg.ieeeAddress), integrationDomain: 'zigbee2mqtt', name: deviceArg.friendlyName,
|
|
57
|
+
id: this.deviceId(snapshotArg.homeId, deviceArg.ieeeAddress), integrationDomain: 'zigbee2mqtt', name: deviceArg.friendlyName,
|
|
58
58
|
manufacturer: deviceArg.manufacturer, model: deviceArg.model, protocol: 'zigbee',
|
|
59
59
|
online: this.available(snapshotArg, deviceArg),
|
|
60
60
|
features: deviceArg.exposes.map((exposeArg) => ({
|
|
61
61
|
id: exposeArg.property, name: exposeArg.name, capability: exposeArg.capability,
|
|
62
|
-
readable: exposeArg.readable, writable:
|
|
62
|
+
readable: exposeArg.readable, writable: this.writable(snapshotArg, deviceArg, exposeArg),
|
|
63
63
|
unit: exposeArg.unit,
|
|
64
64
|
})),
|
|
65
65
|
state: deviceArg.exposes.filter((exposeArg) => exposeArg.readable).flatMap((exposeArg) => {
|
|
@@ -67,6 +67,7 @@ export class Zigbee2MqttMapper {
|
|
|
67
67
|
return state ? [{ featureId: exposeArg.property, value: state.value, updatedAt: state.updatedAt }] : [];
|
|
68
68
|
}),
|
|
69
69
|
metadata: {
|
|
70
|
+
homeId: snapshotArg.homeId,
|
|
70
71
|
ieeeAddress: deviceArg.ieeeAddress, supported: deviceArg.supported, disabled: deviceArg.disabled,
|
|
71
72
|
unsupportedExposes: deviceArg.unsupportedExposes,
|
|
72
73
|
availability: snapshotArg.availability.has(deviceArg.ieeeAddress) ? snapshotArg.availability.get(deviceArg.ieeeAddress) ? 'online' : 'offline' : 'unknown',
|
|
@@ -76,18 +77,23 @@ export class Zigbee2MqttMapper {
|
|
|
76
77
|
|
|
77
78
|
public static toEntities(snapshotArg: IZigbee2MqttSnapshot): IIntegrationEntity[] {
|
|
78
79
|
return snapshotArg.devices.flatMap((deviceArg) => deviceArg.exposes.map((exposeArg) => ({
|
|
79
|
-
id: this.entityId(deviceArg.ieeeAddress, exposeArg.property),
|
|
80
|
-
uniqueId: this.entityId(deviceArg.ieeeAddress, exposeArg.property),
|
|
81
|
-
integrationDomain: 'zigbee2mqtt', deviceId: this.deviceId(deviceArg.ieeeAddress),
|
|
80
|
+
id: this.entityId(snapshotArg.homeId, deviceArg.ieeeAddress, exposeArg.property),
|
|
81
|
+
uniqueId: this.entityId(snapshotArg.homeId, deviceArg.ieeeAddress, exposeArg.property),
|
|
82
|
+
integrationDomain: 'zigbee2mqtt', deviceId: this.deviceId(snapshotArg.homeId, deviceArg.ieeeAddress),
|
|
82
83
|
name: `${deviceArg.friendlyName} ${exposeArg.name}`, platform: exposeArg.platform,
|
|
83
84
|
available: this.available(snapshotArg, deviceArg),
|
|
84
85
|
state: exposeArg.readable ? snapshotArg.state.get(deviceArg.ieeeAddress)?.get(exposeArg.property)?.value ?? null : null,
|
|
85
|
-
attributes: { property: exposeArg.property, readable: exposeArg.readable, writable:
|
|
86
|
+
attributes: { homeId: snapshotArg.homeId, property: exposeArg.property, readable: exposeArg.readable, writable: this.writable(snapshotArg, deviceArg, exposeArg),
|
|
86
87
|
unit: exposeArg.unit, minimum: exposeArg.minimum, maximum: exposeArg.maximum, step: exposeArg.step,
|
|
87
88
|
values: exposeArg.values ? [...exposeArg.values] : undefined },
|
|
88
89
|
})));
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
public static writable(snapshotArg: IZigbee2MqttSnapshot, deviceArg: IZigbee2MqttDevice, exposeArg: IZigbee2MqttExpose): boolean {
|
|
93
|
+
return snapshotArg.inventoryCurrent && exposeArg.writable && deviceArg.supported && !deviceArg.disabled &&
|
|
94
|
+
snapshotArg.controls.has(zigbee2MqttControlKey(deviceArg.ieeeAddress, exposeArg.property));
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
public static available(snapshotArg: IZigbee2MqttSnapshot, deviceArg: IZigbee2MqttDevice): boolean {
|
|
92
98
|
return snapshotArg.connected && snapshotArg.bridgeOnline && snapshotArg.inventoryCurrent &&
|
|
93
99
|
deviceArg.supported && !deviceArg.disabled && snapshotArg.availability.get(deviceArg.ieeeAddress) === true;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { TEntityPlatform } from '../../core/types.js';
|
|
2
2
|
import type * as plugins from '../../plugins.js';
|
|
3
|
+
import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
|
|
4
|
+
import type { IMqttCredentialRequest, IMqttTransportCredentials } from '../mqtt/mqtt.types.js';
|
|
3
5
|
|
|
4
6
|
export interface IZigbee2MqttConfig {
|
|
7
|
+
homeId: string;
|
|
8
|
+
authentication: 'anonymous' | 'provider' | 'config-store';
|
|
9
|
+
credentialKey?: string;
|
|
10
|
+
allowUnencryptedLan?: boolean;
|
|
5
11
|
host: string;
|
|
6
12
|
baseTopic: string;
|
|
7
13
|
port?: number;
|
|
8
14
|
protocol?: 'mqtt' | 'mqtts';
|
|
9
|
-
username?: string;
|
|
10
|
-
password?: string;
|
|
11
15
|
clientId?: string;
|
|
12
16
|
connectTimeoutMs?: number;
|
|
13
17
|
operationTimeoutMs?: number;
|
|
@@ -18,6 +22,33 @@ export interface IZigbee2MqttConfig {
|
|
|
18
22
|
maxBufferedBytes?: number;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/** Runtime-only owner permission; it is never learned from MQTT or editable config. */
|
|
26
|
+
export interface IZigbee2MqttControlPermission {
|
|
27
|
+
readonly ieeeAddress: string;
|
|
28
|
+
readonly property: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IZigbee2MqttCredentialRequest extends IMqttCredentialRequest {
|
|
32
|
+
readonly host: string;
|
|
33
|
+
readonly port: number;
|
|
34
|
+
readonly protocol: 'mqtt' | 'mqtts';
|
|
35
|
+
readonly baseTopic: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface IZigbee2MqttStoredCredential {
|
|
39
|
+
version: 1;
|
|
40
|
+
host: string;
|
|
41
|
+
port: number;
|
|
42
|
+
protocol: 'mqtt' | 'mqtts';
|
|
43
|
+
baseTopic: string;
|
|
44
|
+
credentials: IMqttTransportCredentials;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IZigbee2MqttIntegrationOptions extends INativeIntegrationOwnerOptions {
|
|
48
|
+
readonly controls?: readonly IZigbee2MqttControlPermission[];
|
|
49
|
+
readonly resolveCredentials?: (requestArg: IZigbee2MqttCredentialRequest) => Promise<IMqttTransportCredentials>;
|
|
50
|
+
}
|
|
51
|
+
|
|
21
52
|
export type TZigbee2MqttPrimitive = string | number | boolean;
|
|
22
53
|
|
|
23
54
|
export interface IZigbee2MqttExpose {
|
|
@@ -54,6 +85,8 @@ export interface IZigbee2MqttReportedState {
|
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
export interface IZigbee2MqttSnapshot {
|
|
88
|
+
homeId: string;
|
|
89
|
+
controls: ReadonlySet<string>;
|
|
57
90
|
devices: IZigbee2MqttDevice[];
|
|
58
91
|
connected: boolean;
|
|
59
92
|
bridgeOnline: boolean;
|