@smarthome.exchange/integrations 1.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 +84 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/classes.configstore.d.ts +2 -0
- package/dist_ts/core/classes.configstore.js +84 -27
- package/dist_ts/core/classes.nativehomeadmission.d.ts +19 -0
- package/dist_ts/core/classes.nativehomeadmission.js +84 -0
- package/dist_ts/core/classes.nativeintegrationowner.d.ts +81 -0
- package/dist_ts/core/classes.nativeintegrationowner.js +273 -0
- package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
- package/dist_ts/core/classes.runtimemanager.js +11 -3
- package/dist_ts/core/index.d.ts +2 -0
- package/dist_ts/core/index.js +3 -1
- package/dist_ts/core/types.d.ts +16 -0
- package/dist_ts/integrations/braviatv/braviatv.classes.client.d.ts +23 -72
- package/dist_ts/integrations/braviatv/braviatv.classes.client.js +265 -625
- package/dist_ts/integrations/braviatv/braviatv.classes.configflow.d.ts +7 -7
- package/dist_ts/integrations/braviatv/braviatv.classes.configflow.js +51 -68
- package/dist_ts/integrations/braviatv/braviatv.classes.integration.d.ts +8 -11
- package/dist_ts/integrations/braviatv/braviatv.classes.integration.js +186 -135
- package/dist_ts/integrations/braviatv/braviatv.discovery.js +12 -11
- package/dist_ts/integrations/braviatv/braviatv.mapper.d.ts +6 -14
- package/dist_ts/integrations/braviatv/braviatv.mapper.js +37 -131
- package/dist_ts/integrations/braviatv/braviatv.types.d.ts +55 -133
- package/dist_ts/integrations/braviatv/braviatv.validation.d.ts +7 -0
- package/dist_ts/integrations/braviatv/braviatv.validation.js +72 -0
- package/dist_ts/integrations/broadlink/broadlink.actions.d.ts +2 -0
- package/dist_ts/integrations/broadlink/broadlink.actions.js +36 -0
- package/dist_ts/integrations/broadlink/broadlink.classes.client.d.ts +21 -19
- package/dist_ts/integrations/broadlink/broadlink.classes.client.js +185 -124
- package/dist_ts/integrations/broadlink/broadlink.classes.configflow.d.ts +5 -4
- package/dist_ts/integrations/broadlink/broadlink.classes.configflow.js +64 -83
- package/dist_ts/integrations/broadlink/broadlink.classes.integration.d.ts +16 -19
- package/dist_ts/integrations/broadlink/broadlink.classes.integration.js +114 -59
- package/dist_ts/integrations/broadlink/broadlink.mapper.d.ts +3 -2
- package/dist_ts/integrations/broadlink/broadlink.mapper.js +2 -1
- package/dist_ts/integrations/broadlink/broadlink.selected.mapper.d.ts +10 -0
- package/dist_ts/integrations/broadlink/broadlink.selected.mapper.js +41 -0
- package/dist_ts/integrations/broadlink/broadlink.types.d.ts +43 -7
- package/dist_ts/integrations/broadlink/broadlink.validation.d.ts +7 -0
- package/dist_ts/integrations/broadlink/broadlink.validation.js +63 -0
- package/dist_ts/integrations/daikin/daikin.classes.client.d.ts +10 -52
- package/dist_ts/integrations/daikin/daikin.classes.client.js +55 -614
- package/dist_ts/integrations/daikin/daikin.classes.configflow.d.ts +8 -12
- package/dist_ts/integrations/daikin/daikin.classes.configflow.js +77 -138
- package/dist_ts/integrations/daikin/daikin.classes.integration.d.ts +7 -24
- package/dist_ts/integrations/daikin/daikin.classes.integration.js +175 -67
- package/dist_ts/integrations/daikin/daikin.discovery.d.ts +11 -24
- package/dist_ts/integrations/daikin/daikin.discovery.js +98 -305
- package/dist_ts/integrations/daikin/daikin.mapper.d.ts +6 -7
- package/dist_ts/integrations/daikin/daikin.mapper.js +95 -155
- package/dist_ts/integrations/daikin/daikin.types.d.ts +24 -44
- package/dist_ts/integrations/daikin/daikin.types.js +1 -1
- package/dist_ts/integrations/daikin/daikin.validation.d.ts +5 -0
- package/dist_ts/integrations/daikin/daikin.validation.js +53 -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/denonavr/denonavr.classes.client.d.ts +31 -40
- package/dist_ts/integrations/denonavr/denonavr.classes.client.js +149 -404
- package/dist_ts/integrations/denonavr/denonavr.classes.configflow.d.ts +7 -2
- package/dist_ts/integrations/denonavr/denonavr.classes.configflow.js +44 -36
- package/dist_ts/integrations/denonavr/denonavr.classes.integration.d.ts +6 -11
- package/dist_ts/integrations/denonavr/denonavr.classes.integration.js +192 -113
- package/dist_ts/integrations/denonavr/denonavr.discovery.d.ts +10 -18
- package/dist_ts/integrations/denonavr/denonavr.discovery.js +71 -193
- package/dist_ts/integrations/denonavr/denonavr.mapper.d.ts +5 -13
- package/dist_ts/integrations/denonavr/denonavr.mapper.js +35 -159
- package/dist_ts/integrations/denonavr/denonavr.types.d.ts +55 -90
- package/dist_ts/integrations/denonavr/denonavr.validation.d.ts +10 -0
- package/dist_ts/integrations/denonavr/denonavr.validation.js +86 -0
- 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/kodi/kodi.classes.client.d.ts +11 -45
- package/dist_ts/integrations/kodi/kodi.classes.client.js +84 -310
- package/dist_ts/integrations/kodi/kodi.classes.configflow.d.ts +9 -8
- package/dist_ts/integrations/kodi/kodi.classes.configflow.js +53 -48
- package/dist_ts/integrations/kodi/kodi.classes.integration.d.ts +11 -11
- package/dist_ts/integrations/kodi/kodi.classes.integration.js +114 -171
- package/dist_ts/integrations/kodi/kodi.discovery.js +53 -29
- package/dist_ts/integrations/kodi/kodi.mapper.d.ts +7 -2
- package/dist_ts/integrations/kodi/kodi.mapper.js +48 -10
- package/dist_ts/integrations/kodi/kodi.types.d.ts +32 -28
- package/dist_ts/integrations/kodi/kodi.validation.d.ts +9 -0
- package/dist_ts/integrations/kodi/kodi.validation.js +62 -0
- package/dist_ts/integrations/modbus/modbus.classes.client.d.ts +15 -61
- package/dist_ts/integrations/modbus/modbus.classes.client.js +117 -776
- package/dist_ts/integrations/modbus/modbus.classes.configflow.d.ts +9 -5
- package/dist_ts/integrations/modbus/modbus.classes.configflow.js +70 -38
- package/dist_ts/integrations/modbus/modbus.classes.integration.d.ts +6 -17
- package/dist_ts/integrations/modbus/modbus.classes.integration.js +133 -57
- package/dist_ts/integrations/modbus/modbus.discovery.d.ts +5 -8
- package/dist_ts/integrations/modbus/modbus.discovery.js +38 -93
- package/dist_ts/integrations/modbus/modbus.mapper.d.ts +8 -32
- package/dist_ts/integrations/modbus/modbus.mapper.js +71 -431
- package/dist_ts/integrations/modbus/modbus.types.d.ts +82 -332
- package/dist_ts/integrations/modbus/modbus.validation.d.ts +10 -0
- package/dist_ts/integrations/modbus/modbus.validation.js +128 -0
- 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/nanoleaf/nanoleaf.classes.client.d.ts +12 -34
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.js +143 -231
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.js +105 -18
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.d.ts +8 -8
- package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.js +141 -155
- package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.d.ts +9 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.js +35 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.d.ts +6 -18
- package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.js +45 -220
- package/dist_ts/integrations/nanoleaf/nanoleaf.types.d.ts +33 -147
- package/dist_ts/integrations/nanoleaf/nanoleaf.validation.d.ts +6 -0
- package/dist_ts/integrations/nanoleaf/nanoleaf.validation.js +82 -0
- package/dist_ts/integrations/onkyo/onkyo.classes.client.d.ts +15 -20
- package/dist_ts/integrations/onkyo/onkyo.classes.client.js +161 -404
- package/dist_ts/integrations/onkyo/onkyo.classes.configflow.d.ts +9 -4
- package/dist_ts/integrations/onkyo/onkyo.classes.configflow.js +51 -6
- package/dist_ts/integrations/onkyo/onkyo.classes.integration.d.ts +22 -6
- package/dist_ts/integrations/onkyo/onkyo.classes.integration.js +144 -11
- package/dist_ts/integrations/onkyo/onkyo.discovery.d.ts +6 -1
- package/dist_ts/integrations/onkyo/onkyo.discovery.js +40 -4
- package/dist_ts/integrations/onkyo/onkyo.mapper.d.ts +7 -8
- package/dist_ts/integrations/onkyo/onkyo.mapper.js +45 -12
- package/dist_ts/integrations/onkyo/onkyo.types.d.ts +58 -8
- package/dist_ts/integrations/onkyo/onkyo.types.js +13 -102
- package/dist_ts/integrations/onkyo/onkyo.validation.d.ts +16 -0
- package/dist_ts/integrations/onkyo/onkyo.validation.js +77 -0
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.d.ts +25 -25
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.js +116 -360
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.d.ts +11 -3
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.js +76 -6
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.d.ts +26 -4
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.js +142 -9
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.d.ts +2 -1
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.js +31 -4
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.d.ts +9 -9
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.js +31 -149
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.d.ts +51 -11
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.js +22 -87
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.d.ts +6 -0
- package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.js +56 -0
- package/dist_ts/integrations/pioneer/pioneer.classes.client.d.ts +31 -16
- package/dist_ts/integrations/pioneer/pioneer.classes.client.js +167 -409
- package/dist_ts/integrations/pioneer/pioneer.classes.configflow.d.ts +10 -3
- package/dist_ts/integrations/pioneer/pioneer.classes.configflow.js +66 -6
- package/dist_ts/integrations/pioneer/pioneer.classes.integration.d.ts +13 -5
- package/dist_ts/integrations/pioneer/pioneer.classes.integration.js +239 -11
- package/dist_ts/integrations/pioneer/pioneer.discovery.d.ts +14 -1
- package/dist_ts/integrations/pioneer/pioneer.discovery.js +51 -4
- package/dist_ts/integrations/pioneer/pioneer.mapper.d.ts +14 -8
- package/dist_ts/integrations/pioneer/pioneer.mapper.js +54 -12
- package/dist_ts/integrations/pioneer/pioneer.types.d.ts +79 -12
- package/dist_ts/integrations/pioneer/pioneer.types.js +12 -76
- package/dist_ts/integrations/pioneer/pioneer.validation.d.ts +10 -0
- package/dist_ts/integrations/pioneer/pioneer.validation.js +85 -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/soundtouch/soundtouch.classes.client.d.ts +13 -36
- package/dist_ts/integrations/soundtouch/soundtouch.classes.client.js +179 -502
- package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.js +50 -40
- package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.d.ts +14 -15
- package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.js +108 -128
- package/dist_ts/integrations/soundtouch/soundtouch.discovery.d.ts +2 -20
- package/dist_ts/integrations/soundtouch/soundtouch.discovery.js +23 -113
- package/dist_ts/integrations/soundtouch/soundtouch.mapper.d.ts +3 -12
- package/dist_ts/integrations/soundtouch/soundtouch.mapper.js +35 -189
- package/dist_ts/integrations/soundtouch/soundtouch.types.d.ts +57 -142
- package/dist_ts/integrations/soundtouch/soundtouch.types.js +18 -3
- package/dist_ts/integrations/soundtouch/soundtouch.validation.d.ts +22 -0
- package/dist_ts/integrations/soundtouch/soundtouch.validation.js +65 -0
- 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/wiz/index.d.ts +1 -0
- package/dist_ts/integrations/wiz/index.js +2 -1
- package/dist_ts/integrations/wiz/wiz.classes.client.d.ts +14 -28
- package/dist_ts/integrations/wiz/wiz.classes.client.js +93 -229
- package/dist_ts/integrations/wiz/wiz.classes.configflow.d.ts +6 -4
- package/dist_ts/integrations/wiz/wiz.classes.configflow.js +38 -67
- package/dist_ts/integrations/wiz/wiz.classes.integration.d.ts +10 -21
- package/dist_ts/integrations/wiz/wiz.classes.integration.js +204 -44
- package/dist_ts/integrations/wiz/wiz.discovery.d.ts +10 -31
- package/dist_ts/integrations/wiz/wiz.discovery.js +48 -249
- package/dist_ts/integrations/wiz/wiz.mapper.d.ts +7 -51
- package/dist_ts/integrations/wiz/wiz.mapper.js +82 -668
- package/dist_ts/integrations/wiz/wiz.types.d.ts +34 -208
- package/dist_ts/integrations/wiz/wiz.validation.d.ts +8 -0
- package/dist_ts/integrations/wiz/wiz.validation.js +72 -0
- package/dist_ts/integrations/yamaha/index.d.ts +1 -0
- package/dist_ts/integrations/yamaha/index.js +2 -1
- package/dist_ts/integrations/yamaha/yamaha.classes.client.d.ts +30 -4
- package/dist_ts/integrations/yamaha/yamaha.classes.client.js +162 -6
- package/dist_ts/integrations/yamaha/yamaha.classes.configflow.d.ts +10 -3
- package/dist_ts/integrations/yamaha/yamaha.classes.configflow.js +106 -6
- package/dist_ts/integrations/yamaha/yamaha.classes.integration.d.ts +22 -4
- package/dist_ts/integrations/yamaha/yamaha.classes.integration.js +243 -8
- package/dist_ts/integrations/yamaha/yamaha.discovery.d.ts +13 -1
- package/dist_ts/integrations/yamaha/yamaha.discovery.js +57 -4
- package/dist_ts/integrations/yamaha/yamaha.mapper.d.ts +16 -8
- package/dist_ts/integrations/yamaha/yamaha.mapper.js +58 -12
- package/dist_ts/integrations/yamaha/yamaha.types.d.ts +73 -6
- package/dist_ts/integrations/yamaha/yamaha.types.js +8 -94
- package/dist_ts/integrations/yamaha/yamaha.validation.d.ts +12 -0
- package/dist_ts/integrations/yamaha/yamaha.validation.js +89 -0
- 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 +9 -9
- package/readme.md +539 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/classes.configstore.ts +84 -27
- package/ts/core/classes.nativehomeadmission.ts +96 -0
- package/ts/core/classes.nativeintegrationowner.ts +302 -0
- package/ts/core/classes.runtimemanager.ts +11 -2
- package/ts/core/index.ts +2 -0
- package/ts/core/types.ts +18 -0
- package/ts/integrations/braviatv/braviatv.classes.client.ts +245 -709
- package/ts/integrations/braviatv/braviatv.classes.configflow.ts +47 -76
- package/ts/integrations/braviatv/braviatv.classes.integration.ts +167 -147
- package/ts/integrations/braviatv/braviatv.discovery.ts +11 -10
- package/ts/integrations/braviatv/braviatv.mapper.ts +39 -142
- package/ts/integrations/braviatv/braviatv.types.ts +53 -173
- package/ts/integrations/braviatv/braviatv.validation.ts +68 -0
- package/ts/integrations/broadlink/broadlink.actions.ts +31 -0
- package/ts/integrations/broadlink/broadlink.classes.client.ts +170 -144
- package/ts/integrations/broadlink/broadlink.classes.configflow.ts +59 -89
- package/ts/integrations/broadlink/broadlink.classes.integration.ts +110 -64
- package/ts/integrations/broadlink/broadlink.mapper.ts +5 -4
- package/ts/integrations/broadlink/broadlink.selected.mapper.ts +46 -0
- package/ts/integrations/broadlink/broadlink.types.ts +48 -8
- package/ts/integrations/broadlink/broadlink.validation.ts +66 -0
- package/ts/integrations/daikin/daikin.classes.client.ts +52 -661
- package/ts/integrations/daikin/daikin.classes.configflow.ts +72 -152
- package/ts/integrations/daikin/daikin.classes.integration.ts +166 -71
- package/ts/integrations/daikin/daikin.discovery.ts +94 -346
- package/ts/integrations/daikin/daikin.mapper.ts +83 -166
- package/ts/integrations/daikin/daikin.types.ts +27 -48
- package/ts/integrations/daikin/daikin.validation.ts +51 -0
- package/ts/integrations/daikin/readme.md +83 -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/denonavr/denonavr.classes.client.ts +132 -421
- package/ts/integrations/denonavr/denonavr.classes.configflow.ts +44 -37
- package/ts/integrations/denonavr/denonavr.classes.integration.ts +171 -117
- package/ts/integrations/denonavr/denonavr.discovery.ts +67 -220
- package/ts/integrations/denonavr/denonavr.mapper.ts +36 -182
- package/ts/integrations/denonavr/denonavr.types.ts +48 -111
- package/ts/integrations/denonavr/denonavr.validation.ts +87 -0
- 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/kodi/kodi.classes.client.ts +72 -352
- package/ts/integrations/kodi/kodi.classes.configflow.ts +52 -55
- package/ts/integrations/kodi/kodi.classes.integration.ts +97 -178
- package/ts/integrations/kodi/kodi.discovery.ts +49 -28
- package/ts/integrations/kodi/kodi.mapper.ts +52 -10
- package/ts/integrations/kodi/kodi.types.ts +33 -58
- package/ts/integrations/kodi/kodi.validation.ts +60 -0
- package/ts/integrations/modbus/modbus.classes.client.ts +100 -842
- package/ts/integrations/modbus/modbus.classes.configflow.ts +70 -41
- package/ts/integrations/modbus/modbus.classes.integration.ts +115 -60
- package/ts/integrations/modbus/modbus.discovery.ts +38 -106
- package/ts/integrations/modbus/modbus.mapper.ts +64 -467
- package/ts/integrations/modbus/modbus.types.ts +64 -381
- package/ts/integrations/modbus/modbus.validation.ts +119 -0
- 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/nanoleaf/nanoleaf.classes.client.ts +115 -249
- package/ts/integrations/nanoleaf/nanoleaf.classes.configflow.ts +90 -19
- package/ts/integrations/nanoleaf/nanoleaf.classes.integration.ts +130 -181
- package/ts/integrations/nanoleaf/nanoleaf.credentials.ts +39 -0
- package/ts/integrations/nanoleaf/nanoleaf.mapper.ts +47 -260
- package/ts/integrations/nanoleaf/nanoleaf.types.ts +29 -147
- package/ts/integrations/nanoleaf/nanoleaf.validation.ts +77 -0
- package/ts/integrations/onkyo/onkyo.classes.client.ts +148 -457
- package/ts/integrations/onkyo/onkyo.classes.configflow.ts +52 -6
- package/ts/integrations/onkyo/onkyo.classes.integration.ts +121 -12
- package/ts/integrations/onkyo/onkyo.discovery.ts +41 -3
- package/ts/integrations/onkyo/onkyo.mapper.ts +46 -19
- package/ts/integrations/onkyo/onkyo.types.ts +55 -110
- package/ts/integrations/onkyo/onkyo.validation.ts +85 -0
- package/ts/integrations/onkyo/readme.md +126 -0
- package/ts/integrations/panasonic_viera/panasonic_viera.classes.client.ts +101 -398
- package/ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.ts +69 -5
- package/ts/integrations/panasonic_viera/panasonic_viera.classes.integration.ts +141 -9
- package/ts/integrations/panasonic_viera/panasonic_viera.discovery.ts +32 -3
- package/ts/integrations/panasonic_viera/panasonic_viera.mapper.ts +34 -161
- package/ts/integrations/panasonic_viera/panasonic_viera.types.ts +52 -96
- package/ts/integrations/panasonic_viera/panasonic_viera.validation.ts +56 -0
- package/ts/integrations/pioneer/pioneer.classes.client.ts +132 -428
- package/ts/integrations/pioneer/pioneer.classes.configflow.ts +67 -5
- package/ts/integrations/pioneer/pioneer.classes.integration.ts +225 -11
- package/ts/integrations/pioneer/pioneer.discovery.ts +45 -3
- package/ts/integrations/pioneer/pioneer.mapper.ts +57 -13
- package/ts/integrations/pioneer/pioneer.types.ts +87 -85
- package/ts/integrations/pioneer/pioneer.validation.ts +84 -0
- 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/soundtouch/soundtouch.classes.client.ts +152 -571
- package/ts/integrations/soundtouch/soundtouch.classes.configflow.ts +47 -42
- package/ts/integrations/soundtouch/soundtouch.classes.integration.ts +95 -150
- package/ts/integrations/soundtouch/soundtouch.discovery.ts +21 -133
- package/ts/integrations/soundtouch/soundtouch.mapper.ts +36 -207
- package/ts/integrations/soundtouch/soundtouch.types.ts +50 -195
- package/ts/integrations/soundtouch/soundtouch.validation.ts +60 -0
- 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/wiz/index.ts +1 -0
- package/ts/integrations/wiz/wiz.classes.client.ts +84 -254
- package/ts/integrations/wiz/wiz.classes.configflow.ts +39 -70
- package/ts/integrations/wiz/wiz.classes.integration.ts +189 -45
- package/ts/integrations/wiz/wiz.discovery.ts +48 -291
- package/ts/integrations/wiz/wiz.mapper.ts +81 -754
- package/ts/integrations/wiz/wiz.types.ts +24 -224
- package/ts/integrations/wiz/wiz.validation.ts +73 -0
- package/ts/integrations/yamaha/index.ts +1 -0
- package/ts/integrations/yamaha/yamaha.classes.client.ts +150 -6
- package/ts/integrations/yamaha/yamaha.classes.configflow.ts +100 -5
- package/ts/integrations/yamaha/yamaha.classes.integration.ts +236 -8
- package/ts/integrations/yamaha/yamaha.discovery.ts +52 -3
- package/ts/integrations/yamaha/yamaha.mapper.ts +65 -13
- package/ts/integrations/yamaha/yamaha.types.ts +77 -97
- package/ts/integrations/yamaha/yamaha.validation.ts +86 -0
- 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,15 +1,21 @@
|
|
|
1
1
|
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
|
2
|
+
import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
|
|
2
3
|
import type { IIntegrationSetupContext } from '../../core/types.js';
|
|
3
4
|
import { createTasmotaDiscoveryDescriptor, TasmotaConfigFlow } from './tasmota.discovery.js';
|
|
4
5
|
import { TasmotaRuntime } from './tasmota.classes.runtime.js';
|
|
5
6
|
import type { IHomeAssistantTasmotaConfig } from './tasmota.types.js';
|
|
7
|
+
import type { ITasmotaIntegrationOptions } from './tasmota.types.js';
|
|
8
|
+
import { normalizeTasmotaConfig } from './tasmota.config.js';
|
|
9
|
+
import { tasmotaOptions } from './tasmota.credentials.js';
|
|
6
10
|
|
|
7
11
|
export class HomeAssistantTasmotaIntegration extends BaseIntegration<IHomeAssistantTasmotaConfig> {
|
|
8
12
|
public readonly domain = 'tasmota';
|
|
9
13
|
public readonly displayName = 'Tasmota';
|
|
10
14
|
public readonly status = 'control-runtime' as const;
|
|
11
15
|
public readonly discoveryDescriptor = createTasmotaDiscoveryDescriptor();
|
|
12
|
-
public readonly configFlow
|
|
16
|
+
public readonly configFlow: TasmotaConfigFlow;
|
|
17
|
+
private readonly owner: NativeIntegrationOwner;
|
|
18
|
+
private readonly options: ITasmotaIntegrationOptions;
|
|
13
19
|
public readonly metadata = {
|
|
14
20
|
source: 'home-assistant/core',
|
|
15
21
|
upstreamPath: 'homeassistant/components/tasmota',
|
|
@@ -19,12 +25,24 @@ export class HomeAssistantTasmotaIntegration extends BaseIntegration<IHomeAssist
|
|
|
19
25
|
scope: 'selected-device-relays-brightness-sensors',
|
|
20
26
|
};
|
|
21
27
|
|
|
28
|
+
constructor(optionsArg: ITasmotaIntegrationOptions = {}) {
|
|
29
|
+
super();
|
|
30
|
+
this.options = tasmotaOptions(optionsArg);
|
|
31
|
+
this.owner = new NativeIntegrationOwner(this.domain, { homeId: this.options.homeId, capacity: this.options.capacity });
|
|
32
|
+
this.configFlow = new TasmotaConfigFlow(this.options, this.owner);
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
public async setup(configArg: IHomeAssistantTasmotaConfig, contextArg: IIntegrationSetupContext): Promise<TasmotaRuntime> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
const config = normalizeTasmotaConfig(configArg);
|
|
37
|
+
return this.owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
|
|
38
|
+
const runtime = new TasmotaRuntime(config, leaseArg, this.options, this.owner);
|
|
39
|
+
leaseArg.own(() => runtime.dispose());
|
|
40
|
+
await runtime.start(leaseArg.signal);
|
|
41
|
+
leaseArg.promoteToRuntime();
|
|
42
|
+
return runtime;
|
|
43
|
+
});
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
public
|
|
46
|
+
public destroy(): Promise<void> { return this.owner.close(); }
|
|
47
|
+
public retryCleanup(): Promise<void> { return this.owner.retryCleanup(); }
|
|
30
48
|
}
|
|
@@ -1,32 +1,43 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { DeviceCommunicationError } from '../../core/errors.js';
|
|
3
|
+
import type { INativeSetupLease, NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
|
|
3
4
|
import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IServiceCallResult } from '../../core/types.js';
|
|
4
|
-
import { MqttClient } from '../mqtt/mqtt.classes.client.js';
|
|
5
|
-
import
|
|
5
|
+
import type { MqttClient } from '../mqtt/mqtt.classes.client.js';
|
|
6
|
+
import { MqttSession } from '../mqtt/mqtt.classes.session.js';
|
|
7
|
+
import type { IMqttMessage, TMqttCredentialResolver } from '../mqtt/mqtt.types.js';
|
|
6
8
|
import { parseTasmotaRecord, tasmotaRecord, TasmotaMapper } from './tasmota.mapper.js';
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
9
|
+
import { tasmotaBrokerIdentity, tasmotaCredential, tasmotaCredentialKey } from './tasmota.credentials.js';
|
|
10
|
+
import type { ITasmotaConfig, ITasmotaDeviceState, ITasmotaIntegrationOptions, ITasmotaSelectedDevice } from './tasmota.types.js';
|
|
9
11
|
|
|
10
12
|
export class TasmotaRuntime implements IIntegrationRuntime {
|
|
11
13
|
public readonly domain = 'tasmota';
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
+
public readonly homeId: string;
|
|
15
|
+
private session?: MqttSession;
|
|
16
|
+
private client?: MqttClient;
|
|
14
17
|
private readonly lifetime = new AbortController();
|
|
15
18
|
private readonly states = new Map<string, ITasmotaDeviceState>();
|
|
16
19
|
private readonly listeners = new Set<(eventArg: IIntegrationEvent) => void>();
|
|
17
20
|
private readonly changed = new Set<() => void>();
|
|
18
21
|
private readonly subscriptionCleanups = new Set<() => void>();
|
|
19
|
-
private
|
|
22
|
+
private stopConnectionListener?: () => void;
|
|
23
|
+
private readonly operations = new Set<Promise<unknown>>();
|
|
20
24
|
private initializing = true;
|
|
21
25
|
private started = false;
|
|
22
26
|
private destroyed = false;
|
|
23
27
|
private destroyPromise?: Promise<void>;
|
|
28
|
+
private disposePromise?: Promise<void>;
|
|
29
|
+
private cleanupFailed = false;
|
|
24
30
|
|
|
25
|
-
constructor(
|
|
26
|
-
|
|
27
|
-
this.
|
|
31
|
+
constructor(private readonly config: ITasmotaConfig, private readonly lease: INativeSetupLease,
|
|
32
|
+
private readonly options: ITasmotaIntegrationOptions, private readonly owner: NativeIntegrationOwner) {
|
|
33
|
+
this.homeId = lease.context.homeId;
|
|
34
|
+
lease.assertOpen();
|
|
35
|
+
if (config.homeId !== this.homeId) throw new TypeError('Tasmota runtime must match its admitted host home.');
|
|
28
36
|
for (const device of this.config.devices) this.states.set(device.id, { generation: 0, ready: false, online: false, queryPending: false, updatedAt: new Date(0).toISOString(), controls: {}, sensors: {} });
|
|
29
|
-
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private observeConnection(clientArg: MqttClient): void {
|
|
40
|
+
this.stopConnectionListener = clientArg.onConnectionState((stateArg) => {
|
|
30
41
|
if (this.destroyed) return;
|
|
31
42
|
if (!stateArg.connected) {
|
|
32
43
|
for (const device of this.config.devices) this.markOffline(device);
|
|
@@ -41,33 +52,50 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
41
52
|
this.checkActive(abortSignalArg);
|
|
42
53
|
if (this.started) throw new DeviceCommunicationError('Tasmota runtime has already started.');
|
|
43
54
|
this.started = true;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const deadlineMonotonicMs = performance.now() + this.config.initialSnapshotTimeoutMs;
|
|
56
|
+
await this.withDeadline(async (signalArg) => {
|
|
57
|
+
const broker = tasmotaBrokerIdentity(this.config);
|
|
58
|
+
const reference = this.config.authentication.mode === 'credential-reference' ? this.config.authentication.credentialRef : undefined;
|
|
59
|
+
const resolver = this.options.credentialResolver;
|
|
60
|
+
const store = this.lease.context.configStore;
|
|
61
|
+
if (reference && !resolver && !store) throw new DeviceCommunicationError('Tasmota credential reference requires an explicit credential source.');
|
|
62
|
+
const resolveCredentials: TMqttCredentialResolver | undefined = reference ? async (requestArg) => {
|
|
63
|
+
const request = Object.freeze({ homeId: requestArg.homeId, credentialRef: requestArg.credentialKey, broker,
|
|
64
|
+
signal: AbortSignal.any([requestArg.signal, signalArg]), deadlineMonotonicMs: Math.min(deadlineMonotonicMs, requestArg.deadline) });
|
|
65
|
+
const credential = resolver ? await resolver(request) : await store!.get<unknown>(tasmotaCredentialKey(requestArg.credentialKey));
|
|
66
|
+
return tasmotaCredential(credential, broker);
|
|
67
|
+
} : undefined;
|
|
68
|
+
this.checkActive(signalArg);
|
|
69
|
+
const remaining = Math.floor(deadlineMonotonicMs - performance.now());
|
|
70
|
+
if (remaining < 1) throw new DeviceCommunicationError('Tasmota fresh state deadline exceeded.');
|
|
71
|
+
const session = new MqttSession({ ...this.config.broker, homeId: this.homeId, credentialKey: reference,
|
|
72
|
+
connectTimeoutMs: Math.min(this.config.broker.connectTimeoutMs ?? remaining, remaining) }, this.lease, resolveCredentials);
|
|
73
|
+
// The runtime's lease owns dispose() before session startup enters the provider or allocates transport.
|
|
74
|
+
this.session = session;
|
|
75
|
+
const client = await session.start();
|
|
76
|
+
this.checkActive(signalArg);
|
|
77
|
+
this.client = client;
|
|
78
|
+
this.observeConnection(client);
|
|
79
|
+
for (const device of this.config.devices) {
|
|
80
|
+
await client.subscribe(`${device.statusBase}#`, (messageArg) => this.receive(device, messageArg), signalArg);
|
|
81
|
+
await client.subscribe(`${device.telemetryBase}#`, (messageArg) => this.receive(device, messageArg), signalArg);
|
|
82
|
+
}
|
|
83
|
+
await this.refresh(this.config.devices, signalArg);
|
|
84
|
+
}, abortSignalArg);
|
|
85
|
+
this.initializing = false;
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
public isHealthy(): boolean {
|
|
61
|
-
return !this.destroyed && this.client
|
|
89
|
+
return !this.destroyed && this.client?.isHealthy() === true && this.config.devices.every((deviceArg) => this.ready(deviceArg));
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
public async devices(abortSignalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
|
|
65
93
|
this.checkActive(abortSignalArg);
|
|
66
|
-
return this.config.devices.map((deviceArg) => TasmotaMapper.device(deviceArg, this.state(deviceArg), this.client.
|
|
94
|
+
return this.config.devices.map((deviceArg) => TasmotaMapper.device(deviceArg, this.state(deviceArg), this.client?.connected === true, this.options.enableControls === true));
|
|
67
95
|
}
|
|
68
96
|
|
|
69
97
|
public async entities(): Promise<IIntegrationEntity[]> {
|
|
70
|
-
return (await this.devices()).flatMap((deviceArg) => TasmotaMapper.entities(deviceArg));
|
|
98
|
+
return (await this.devices()).flatMap((deviceArg) => TasmotaMapper.entities(deviceArg, this.homeId));
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, abortSignalArg?: AbortSignal): Promise<() => Promise<void>> {
|
|
@@ -86,10 +114,12 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
86
114
|
|
|
87
115
|
public async writeFeature(requestArg: IDeviceFeatureWriteRequest, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
|
|
88
116
|
this.checkActive(abortSignalArg);
|
|
117
|
+
if (this.options.enableControls !== true) return { success: false, error: 'Tasmota controls require explicit embedding-owner enablement.' };
|
|
89
118
|
if (requestArg.device.integrationDomain !== this.domain) return { success: false, error: 'Tasmota device is not selected.' };
|
|
90
119
|
const device = this.config.devices.find((deviceArg) => deviceArg.id === requestArg.device.id);
|
|
91
120
|
if (!device) return { success: false, error: 'Tasmota device is not selected.' };
|
|
92
|
-
|
|
121
|
+
const client = this.client;
|
|
122
|
+
if (!client?.isHealthy() || !this.ready(device)) return { success: false, error: 'Tasmota device has no fresh online state.' };
|
|
93
123
|
const featureId = requestArg.feature.id;
|
|
94
124
|
let command: string;
|
|
95
125
|
let payload: string;
|
|
@@ -103,22 +133,60 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
103
133
|
} else {
|
|
104
134
|
return { success: false, error: 'Tasmota write requires a selected relay boolean or brightness integer from 0 through 100.' };
|
|
105
135
|
}
|
|
106
|
-
|
|
136
|
+
const operation = this.owner.runOperation(this.lease.context, this.homeId, async (operationArg) => {
|
|
137
|
+
const signal = AbortSignal.any([operationArg.signal, this.lease.signal, this.lifetime.signal, ...(abortSignalArg ? [abortSignalArg] : [])]);
|
|
138
|
+
this.checkActive(signal);
|
|
139
|
+
await client.publish({ topic: `${device.commandBase}${command}`, payload, qos: 0, retain: false }, signal);
|
|
140
|
+
});
|
|
141
|
+
await this.retain(operation);
|
|
107
142
|
return { success: true, data: { accepted: true } };
|
|
108
143
|
}
|
|
109
144
|
|
|
110
145
|
public destroy(): Promise<void> {
|
|
111
|
-
if (this.destroyPromise)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.listeners.clear();
|
|
117
|
-
this.changed.clear();
|
|
118
|
-
this.destroyPromise = this.client.destroy();
|
|
146
|
+
if (!this.destroyPromise) {
|
|
147
|
+
this.destroyed = true;
|
|
148
|
+
this.destroyPromise = Promise.resolve().then(() => this.lease.close());
|
|
149
|
+
this.lifetime.abort(new DeviceCommunicationError('Tasmota runtime destroyed.'));
|
|
150
|
+
}
|
|
119
151
|
return this.destroyPromise;
|
|
120
152
|
}
|
|
121
153
|
|
|
154
|
+
/** Resource drain registered on the native lease, separate from its public closure. */
|
|
155
|
+
public dispose(): Promise<void> {
|
|
156
|
+
if (!this.disposePromise || this.cleanupFailed) {
|
|
157
|
+
const retry = this.cleanupFailed;
|
|
158
|
+
this.cleanupFailed = false;
|
|
159
|
+
this.destroyed = true;
|
|
160
|
+
this.disposePromise = Promise.resolve().then(async () => {
|
|
161
|
+
const cleanup = Promise.allSettled([
|
|
162
|
+
Promise.resolve().then(() => this.stopConnectionListener?.()),
|
|
163
|
+
...[...this.subscriptionCleanups].map((cleanupArg) => Promise.resolve().then(cleanupArg)),
|
|
164
|
+
...(this.session ? [Promise.resolve().then(() => retry ? this.session!.retryCleanup() : this.session!.close())] : []),
|
|
165
|
+
]);
|
|
166
|
+
const [results] = await Promise.all([cleanup, Promise.allSettled([...this.operations])]);
|
|
167
|
+
this.listeners.clear();
|
|
168
|
+
this.changed.clear();
|
|
169
|
+
const failures = results.filter((resultArg): resultArg is PromiseRejectedResult => resultArg.status === 'rejected')
|
|
170
|
+
.map((resultArg) => resultArg.reason as unknown);
|
|
171
|
+
if (failures.length) { this.cleanupFailed = true; throw new AggregateError(failures, 'Tasmota runtime cleanup failed.'); }
|
|
172
|
+
});
|
|
173
|
+
this.lifetime.abort(new DeviceCommunicationError('Tasmota runtime destroyed.'));
|
|
174
|
+
}
|
|
175
|
+
return this.disposePromise;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private track<TValue>(workArg: () => Promise<TValue>): Promise<TValue> {
|
|
179
|
+
this.checkActive();
|
|
180
|
+
const operation = Promise.resolve().then(() => { this.checkActive(); return workArg(); });
|
|
181
|
+
return this.retain(operation);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private retain<TValue>(operationArg: Promise<TValue>): Promise<TValue> {
|
|
185
|
+
this.operations.add(operationArg);
|
|
186
|
+
void operationArg.then(() => this.operations.delete(operationArg), () => this.operations.delete(operationArg));
|
|
187
|
+
return operationArg;
|
|
188
|
+
}
|
|
189
|
+
|
|
122
190
|
private state(deviceArg: ITasmotaSelectedDevice): ITasmotaDeviceState {
|
|
123
191
|
const state = this.states.get(deviceArg.id);
|
|
124
192
|
if (!state) throw new DeviceCommunicationError('Selected Tasmota device state is missing.');
|
|
@@ -133,12 +201,13 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
133
201
|
private checkActive(abortSignalArg?: AbortSignal): void {
|
|
134
202
|
abortSignalArg?.throwIfAborted();
|
|
135
203
|
this.lifetime.signal.throwIfAborted();
|
|
204
|
+
this.lease.assertOpen();
|
|
136
205
|
}
|
|
137
206
|
|
|
138
207
|
private async withDeadline<TValue>(operationArg: (signalArg: AbortSignal) => Promise<TValue>, abortSignalArg?: AbortSignal): Promise<TValue> {
|
|
139
208
|
const deadline = new AbortController();
|
|
140
209
|
const timer = setTimeout(() => deadline.abort(new DeviceCommunicationError('Tasmota fresh state deadline exceeded.')), this.config.initialSnapshotTimeoutMs);
|
|
141
|
-
const signal = AbortSignal.any([this.lifetime.signal, deadline.signal, ...(abortSignalArg ? [abortSignalArg] : [])]);
|
|
210
|
+
const signal = AbortSignal.any([this.lifetime.signal, this.lease.signal, deadline.signal, ...(abortSignalArg ? [abortSignalArg] : [])]);
|
|
142
211
|
try {
|
|
143
212
|
signal.throwIfAborted();
|
|
144
213
|
return await operationArg(signal);
|
|
@@ -160,8 +229,8 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
160
229
|
const fresh = this.waitForFresh(devicesArg, generationsArg, signalArg);
|
|
161
230
|
const queries = async (): Promise<void> => {
|
|
162
231
|
for (const device of devicesArg) {
|
|
163
|
-
await this.client
|
|
164
|
-
await this.client
|
|
232
|
+
await this.client!.publish({ topic: `${device.commandBase}State`, payload: '', qos: 0, retain: false }, signalArg);
|
|
233
|
+
await this.client!.publish({ topic: `${device.commandBase}Status`, payload: '10', qos: 0, retain: false }, signalArg);
|
|
165
234
|
}
|
|
166
235
|
};
|
|
167
236
|
await Promise.all([fresh, queries()]);
|
|
@@ -169,7 +238,7 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
169
238
|
|
|
170
239
|
private refreshInBackground(devicesArg: ITasmotaSelectedDevice[]): void {
|
|
171
240
|
const generations = new Map<string, number>();
|
|
172
|
-
void this.withDeadline((signalArg) => this.refresh(devicesArg, signalArg, generations)).catch((errorArg: unknown) => {
|
|
241
|
+
void this.track(() => this.withDeadline((signalArg) => this.refresh(devicesArg, signalArg, generations))).catch((errorArg: unknown) => {
|
|
173
242
|
if (this.destroyed) return;
|
|
174
243
|
const currentDevices = devicesArg.filter((deviceArg) => this.state(deviceArg).generation === generations.get(deviceArg.id));
|
|
175
244
|
if (!currentDevices.length) return;
|
|
@@ -190,7 +259,7 @@ export class TasmotaRuntime implements IIntegrationRuntime {
|
|
|
190
259
|
reject(new DeviceCommunicationError('Tasmota state refresh superseded by a connection or availability change.'));
|
|
191
260
|
return;
|
|
192
261
|
}
|
|
193
|
-
if (this.client
|
|
262
|
+
if (this.client?.connected && devicesArg.every((deviceArg) => this.ready(deviceArg))) {
|
|
194
263
|
cleanup();
|
|
195
264
|
resolve();
|
|
196
265
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IntegrationError } from '../../core/errors.js';
|
|
2
2
|
import type { IHomeAssistantTasmotaConfig, ITasmotaConfig, ITasmotaDeviceConfig, ITasmotaSelectedDevice } from './tasmota.types.js';
|
|
3
|
+
import { tasmotaCopy, tasmotaData, tasmotaText } from './tasmota.credentials.js';
|
|
3
4
|
|
|
4
5
|
const fail = (messageArg: string): never => {
|
|
5
6
|
throw new IntegrationError(messageArg, 'INVALID_TASMOTA_CONFIG');
|
|
@@ -51,6 +52,20 @@ const selectedDevice = (deviceArg: ITasmotaDeviceConfig): ITasmotaSelectedDevice
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
export const normalizeTasmotaConfig = (configArg: IHomeAssistantTasmotaConfig): ITasmotaConfig => {
|
|
55
|
+
const input = tasmotaData(tasmotaCopy(configArg), 'Tasmota configuration');
|
|
56
|
+
const broker = tasmotaData(input.broker, 'Tasmota broker');
|
|
57
|
+
if (['username', 'password', 'token', 'ca', 'cert', 'key'].some((fieldArg) => broker[fieldArg] !== undefined)) {
|
|
58
|
+
return fail('Legacy plaintext Tasmota broker credentials require explicit reconfiguration with an encrypted credential reference.');
|
|
59
|
+
}
|
|
60
|
+
const homeId = tasmotaText(input.homeId, 'Tasmota configuration home');
|
|
61
|
+
const authentication = tasmotaData(input.authentication, 'Tasmota authentication');
|
|
62
|
+
if (authentication.mode === 'anonymous') {
|
|
63
|
+
if (Object.keys(authentication).some((keyArg) => keyArg !== 'mode')) return fail('Anonymous Tasmota authentication must be selected explicitly without credentials.');
|
|
64
|
+
} else if (authentication.mode === 'credential-reference') {
|
|
65
|
+
if (Object.keys(authentication).some((keyArg) => !['mode', 'credentialRef'].includes(keyArg))) return fail('Tasmota authentication accepts only a credential reference.');
|
|
66
|
+
tasmotaText(authentication.credentialRef, 'Tasmota credential reference', 512);
|
|
67
|
+
} else return fail('Select explicit anonymous authentication or a broker credential reference.');
|
|
68
|
+
configArg = structuredClone(input) as unknown as IHomeAssistantTasmotaConfig;
|
|
54
69
|
if (!configArg || typeof configArg !== 'object' || !configArg.broker || typeof configArg.broker.host !== 'string' || !configArg.broker.host.trim() || configArg.broker.host !== configArg.broker.host.trim()) return fail('Tasmota requires a real MQTT broker host without surrounding whitespace.');
|
|
55
70
|
if (configArg.broker.port !== undefined && (!Number.isInteger(configArg.broker.port) || configArg.broker.port < 1 || configArg.broker.port > 65_535)) return fail('Tasmota broker port must be an integer from 1 through 65535.');
|
|
56
71
|
if (configArg.broker.protocol !== undefined && configArg.broker.protocol !== 'mqtt' && configArg.broker.protocol !== 'mqtts') return fail('Tasmota broker protocol must be mqtt or mqtts.');
|
|
@@ -62,6 +77,8 @@ export const normalizeTasmotaConfig = (configArg: IHomeAssistantTasmotaConfig):
|
|
|
62
77
|
const initialSnapshotTimeoutMs = configArg.initialSnapshotTimeoutMs ?? 10_000;
|
|
63
78
|
if (!Number.isInteger(initialSnapshotTimeoutMs) || initialSnapshotTimeoutMs < 1 || initialSnapshotTimeoutMs > 2_147_483_647) return fail('initialSnapshotTimeoutMs must be a timer-safe positive integer.');
|
|
64
79
|
return {
|
|
80
|
+
homeId,
|
|
81
|
+
authentication: Object.freeze({ ...configArg.authentication }),
|
|
65
82
|
devices,
|
|
66
83
|
initialSnapshotTimeoutMs,
|
|
67
84
|
broker: { ...configArg.broker, discovery: false, birthMessage: false, willMessage: false, subscriptions: [], entities: [], discoveryMessages: [], retainedMessages: [] },
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { IMqttTransportCredentials } from '../mqtt/mqtt.types.js';
|
|
2
|
+
import type { ITasmotaBrokerIdentity, ITasmotaConfig, ITasmotaCredential, ITasmotaIntegrationOptions } from './tasmota.types.js';
|
|
3
|
+
|
|
4
|
+
export const tasmotaData = (valueArg: unknown, nameArg: string): Record<string, unknown> => {
|
|
5
|
+
if (!valueArg || typeof valueArg !== 'object'
|
|
6
|
+
|| (Object.getPrototypeOf(valueArg) !== Object.prototype && Object.getPrototypeOf(valueArg) !== null)) {
|
|
7
|
+
throw new TypeError(`${nameArg} must be a plain data object.`);
|
|
8
|
+
}
|
|
9
|
+
const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
10
|
+
for (const key of Reflect.ownKeys(valueArg)) {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
12
|
+
if (typeof key !== 'string' || !descriptor?.enumerable || !('value' in descriptor)) {
|
|
13
|
+
throw new TypeError(`${nameArg} must not contain accessors or hidden fields.`);
|
|
14
|
+
}
|
|
15
|
+
result[key] = descriptor.value as unknown;
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Capture configuration data without invoking nested user accessors. */
|
|
21
|
+
export const tasmotaCopy = (valueArg: unknown, ancestorsArg = new Set<object>(), depthArg = 0): unknown => {
|
|
22
|
+
if (valueArg === null || valueArg === undefined || typeof valueArg === 'string' || typeof valueArg === 'boolean'
|
|
23
|
+
|| (typeof valueArg === 'number' && Number.isFinite(valueArg))) return valueArg;
|
|
24
|
+
if (!valueArg || typeof valueArg !== 'object' || depthArg > 32 || ancestorsArg.has(valueArg)) throw new TypeError('Tasmota configuration must contain bounded plain data.');
|
|
25
|
+
ancestorsArg.add(valueArg);
|
|
26
|
+
try {
|
|
27
|
+
if (Array.isArray(valueArg)) {
|
|
28
|
+
const items: unknown[] = [];
|
|
29
|
+
for (let index = 0; index < valueArg.length; index += 1) {
|
|
30
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index));
|
|
31
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) throw new TypeError('Tasmota configuration arrays must contain data values.');
|
|
32
|
+
items.push(tasmotaCopy(descriptor.value as unknown, ancestorsArg, depthArg + 1));
|
|
33
|
+
}
|
|
34
|
+
if (Reflect.ownKeys(valueArg).length !== items.length + 1) throw new TypeError('Tasmota configuration arrays contain unsupported fields.');
|
|
35
|
+
return items;
|
|
36
|
+
}
|
|
37
|
+
const data = tasmotaData(valueArg, 'Tasmota configuration');
|
|
38
|
+
return Object.fromEntries(Object.entries(data).map(([keyArg, itemArg]) => [keyArg, tasmotaCopy(itemArg, ancestorsArg, depthArg + 1)]));
|
|
39
|
+
} finally { ancestorsArg.delete(valueArg); }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const tasmotaText = (valueArg: unknown, nameArg: string, maximumArg = 256): string => {
|
|
43
|
+
if (typeof valueArg !== 'string' || !valueArg || valueArg.length > maximumArg
|
|
44
|
+
|| valueArg.trim() !== valueArg || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
|
|
45
|
+
throw new TypeError(`${nameArg} must be a bounded nonempty identifier.`);
|
|
46
|
+
}
|
|
47
|
+
return valueArg;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const tasmotaOptions = (valueArg: ITasmotaIntegrationOptions): ITasmotaIntegrationOptions => {
|
|
51
|
+
const options = tasmotaData(valueArg, 'Tasmota owner options');
|
|
52
|
+
if (Object.keys(options).some((keyArg) => !['homeId', 'capacity', 'enableControls', 'credentialResolver'].includes(keyArg))) {
|
|
53
|
+
throw new TypeError('Tasmota owner options contain unsupported fields.');
|
|
54
|
+
}
|
|
55
|
+
if (options.enableControls !== undefined && typeof options.enableControls !== 'boolean') throw new TypeError('Tasmota enableControls must be boolean.');
|
|
56
|
+
if (options.credentialResolver !== undefined && typeof options.credentialResolver !== 'function') throw new TypeError('Tasmota credential resolver must be a function.');
|
|
57
|
+
return Object.freeze({ ...options }) as ITasmotaIntegrationOptions;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const tasmotaBrokerIdentity = (configArg: ITasmotaConfig): ITasmotaBrokerIdentity => Object.freeze({
|
|
61
|
+
host: configArg.broker.host, port: configArg.broker.port ?? (configArg.broker.protocol === 'mqtts' ? 8883 : 1883),
|
|
62
|
+
protocol: configArg.broker.protocol ?? 'mqtt',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const tasmotaCredentialKey = (referenceArg: string): string => `tasmota.credential.${encodeURIComponent(tasmotaText(referenceArg, 'Credential reference', 512))}`;
|
|
66
|
+
|
|
67
|
+
export const tasmotaCredential = (valueArg: unknown, brokerArg: ITasmotaBrokerIdentity): IMqttTransportCredentials => {
|
|
68
|
+
const value = tasmotaData(valueArg, 'Tasmota credential');
|
|
69
|
+
if (Object.keys(value).some((keyArg) => !['version', 'host', 'port', 'protocol', 'username', 'password', 'ca', 'cert', 'key', 'servername'].includes(keyArg))
|
|
70
|
+
|| value.version !== 1 || value.host !== brokerArg.host || value.port !== brokerArg.port || value.protocol !== brokerArg.protocol) {
|
|
71
|
+
throw new TypeError('Tasmota credential does not match the selected broker.');
|
|
72
|
+
}
|
|
73
|
+
const credentials: IMqttTransportCredentials = {};
|
|
74
|
+
for (const field of ['username', 'password', 'ca', 'cert', 'key', 'servername'] as const) {
|
|
75
|
+
const item = value[field];
|
|
76
|
+
if (item !== undefined) {
|
|
77
|
+
if (typeof item !== 'string' || item.length > 1_048_576 || item.includes('\u0000')) throw new TypeError('Tasmota credential fields are invalid.');
|
|
78
|
+
credentials[field] = item;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!credentials.username && !credentials.password && !credentials.cert) throw new TypeError('Tasmota credential reference must resolve authenticated broker credentials.');
|
|
82
|
+
if ((credentials.cert === undefined) !== (credentials.key === undefined)) throw new TypeError('Tasmota client certificate and private key must be supplied together.');
|
|
83
|
+
if ((credentials.ca || credentials.cert || credentials.key || credentials.servername) && brokerArg.protocol !== 'mqtts') throw new TypeError('Tasmota TLS credentials require mqtts.');
|
|
84
|
+
if (credentials.servername !== undefined) tasmotaText(credentials.servername, 'TLS server name', 253);
|
|
85
|
+
return Object.freeze(credentials);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const createTasmotaCredential = (brokerArg: ITasmotaBrokerIdentity, usernameArg?: string, passwordArg?: string): ITasmotaCredential => {
|
|
89
|
+
const credential: ITasmotaCredential = { version: 1, ...brokerArg, username: usernameArg, password: passwordArg };
|
|
90
|
+
tasmotaCredential(credential, brokerArg);
|
|
91
|
+
return credential;
|
|
92
|
+
};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
2
|
+
import * as plugins from '../../plugins.js';
|
|
3
|
+
import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
|
|
2
4
|
import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
|
|
3
5
|
import { normalizeTasmotaConfig } from './tasmota.config.js';
|
|
4
|
-
import type { IHomeAssistantTasmotaConfig } from './tasmota.types.js';
|
|
6
|
+
import type { IHomeAssistantTasmotaConfig, ITasmotaIntegrationOptions } from './tasmota.types.js';
|
|
7
|
+
import { createTasmotaCredential, tasmotaBrokerIdentity, tasmotaCopy, tasmotaCredentialKey, tasmotaData, tasmotaOptions, tasmotaText } from './tasmota.credentials.js';
|
|
5
8
|
|
|
6
9
|
class TasmotaManualMatcher implements IDiscoveryMatcher<IDiscoveryCandidate> {
|
|
7
10
|
public readonly id = 'tasmota-selected-device';
|
|
@@ -35,8 +38,28 @@ export const createTasmotaDiscoveryDescriptor = (): DiscoveryDescriptor => {
|
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfig> {
|
|
41
|
+
private readonly scope: NativeIntegrationScope;
|
|
42
|
+
private readonly hasResolver: boolean;
|
|
43
|
+
|
|
44
|
+
constructor(optionsArg: ITasmotaIntegrationOptions = {}, ownerArg?: NativeIntegrationOwner) {
|
|
45
|
+
const options = tasmotaOptions(optionsArg);
|
|
46
|
+
const owner = ownerArg ?? new NativeIntegrationOwner('tasmota', { homeId: options.homeId, capacity: options.capacity });
|
|
47
|
+
this.scope = owner.createFlowScope();
|
|
48
|
+
this.hasResolver = options.credentialResolver !== undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public destroy(): Promise<void> { return this.scope.close(); }
|
|
52
|
+
public retryCleanup(): Promise<void> { return this.scope.retryCleanup(); }
|
|
53
|
+
|
|
38
54
|
public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IHomeAssistantTasmotaConfig>> {
|
|
39
|
-
|
|
55
|
+
const candidate = tasmotaData(tasmotaCopy(candidateArg), 'Tasmota candidate');
|
|
56
|
+
const metadata = candidate.metadata === undefined ? undefined : tasmotaData(candidate.metadata, 'Tasmota candidate metadata');
|
|
57
|
+
const context = this.scope.admit(contextArg);
|
|
58
|
+
const candidateHost = candidate.host;
|
|
59
|
+
const candidatePort = candidate.port;
|
|
60
|
+
const candidateTopic = metadata?.topic;
|
|
61
|
+
let pending = false;
|
|
62
|
+
let submitted = false;
|
|
40
63
|
return {
|
|
41
64
|
kind: 'form',
|
|
42
65
|
title: 'Connect a selected Tasmota device',
|
|
@@ -45,6 +68,8 @@ export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfi
|
|
|
45
68
|
{ name: 'host', label: 'MQTT broker host', type: 'text', required: true },
|
|
46
69
|
{ name: 'port', label: 'MQTT broker port', type: 'number' },
|
|
47
70
|
{ name: 'useTls', label: 'Use MQTT TLS', type: 'boolean' },
|
|
71
|
+
{ name: 'anonymous', label: 'Use an anonymous broker', type: 'boolean' },
|
|
72
|
+
{ name: 'credentialRef', label: 'Existing broker credential reference', type: 'text' },
|
|
48
73
|
{ name: 'username', label: 'Broker username', type: 'text' },
|
|
49
74
|
{ name: 'password', label: 'Broker password', type: 'password' },
|
|
50
75
|
{ name: 'topic', label: 'Device Topic', type: 'text', required: true },
|
|
@@ -53,31 +78,69 @@ export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfi
|
|
|
53
78
|
{ name: 'brightness', label: 'Single-channel Dimmer', type: 'boolean' },
|
|
54
79
|
],
|
|
55
80
|
submit: async (valuesArg) => {
|
|
81
|
+
if (pending || submitted) return { kind: 'error', error: 'This Tasmota form is already submitting or completed.' };
|
|
82
|
+
pending = true;
|
|
56
83
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
const values = tasmotaData(tasmotaCopy(valuesArg), 'Tasmota form');
|
|
85
|
+
return await this.scope.run(context, context.homeId, async (leaseArg) => {
|
|
86
|
+
if (Object.keys(values).some((keyArg) => !['homeId', 'host', 'port', 'useTls', 'anonymous', 'credentialRef', 'username', 'password', 'topic', 'fullTopic', 'relayCount', 'brightness'].includes(keyArg))) throw new Error('Tasmota form contains unsupported fields.');
|
|
87
|
+
if (values.homeId !== undefined && values.homeId !== context.homeId) throw new Error('Tasmota form home conflicts with the admitted host home.');
|
|
88
|
+
for (const field of ['username', 'password', 'fullTopic', 'credentialRef']) if (values[field] !== undefined && typeof values[field] !== 'string') throw new Error(`${field} must be a string.`);
|
|
89
|
+
for (const field of ['useTls', 'brightness', 'anonymous']) if (values[field] !== undefined && typeof values[field] !== 'boolean') throw new Error(`${field} must be boolean.`);
|
|
90
|
+
const relayCount = values.relayCount ?? 1;
|
|
91
|
+
if (typeof relayCount !== 'number' || !Number.isInteger(relayCount) || relayCount < 0 || relayCount > 32) throw new Error('relayCount must be an integer from 0 through 32.');
|
|
92
|
+
const host = values.host ?? candidateHost;
|
|
93
|
+
const topic = values.topic ?? candidateTopic;
|
|
94
|
+
if (typeof host !== 'string' || typeof topic !== 'string') throw new Error('Broker host and device topic are required.');
|
|
95
|
+
const port = values.port ?? candidatePort;
|
|
96
|
+
if (port !== undefined && (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535)) throw new Error('Broker port must be an integer from 1 through 65535.');
|
|
97
|
+
const secretInput = values.username !== undefined || values.password !== undefined;
|
|
98
|
+
const anonymous = values.anonymous === true;
|
|
99
|
+
if (anonymous && (secretInput || values.credentialRef !== undefined)) throw new Error('Anonymous Tasmota mode cannot contain credentials.');
|
|
100
|
+
if (!anonymous && !secretInput && values.credentialRef === undefined) throw new Error('Select anonymous mode explicitly or provide broker credentials.');
|
|
101
|
+
if (secretInput && values.credentialRef !== undefined) throw new Error('Provide transient credentials or an existing reference, not both.');
|
|
102
|
+
const reference = anonymous ? undefined : secretInput ? plugins.crypto.randomUUID() : tasmotaText(values.credentialRef, 'Credential reference', 512);
|
|
103
|
+
const config: IHomeAssistantTasmotaConfig = {
|
|
104
|
+
homeId: context.homeId,
|
|
105
|
+
authentication: reference === undefined ? { mode: 'anonymous' } : { mode: 'credential-reference', credentialRef: reference },
|
|
106
|
+
broker: {
|
|
107
|
+
host,
|
|
108
|
+
port,
|
|
109
|
+
protocol: values.useTls === true ? 'mqtts' : 'mqtt',
|
|
110
|
+
},
|
|
111
|
+
devices: [{ topic, fullTopic: typeof values.fullTopic === 'string' && values.fullTopic ? values.fullTopic : undefined, relays: Array.from({ length: relayCount }, (_, indexArg) => indexArg + 1), brightness: values.brightness === true }],
|
|
112
|
+
};
|
|
113
|
+
const normalized = normalizeTasmotaConfig(config);
|
|
114
|
+
if (!anonymous) {
|
|
115
|
+
const store = leaseArg.context.configStore;
|
|
116
|
+
if (secretInput) {
|
|
117
|
+
if (!store) throw new Error('Transient Tasmota broker credentials require an admitted encrypted config store.');
|
|
118
|
+
const credential = createTasmotaCredential(tasmotaBrokerIdentity(normalized),
|
|
119
|
+
typeof values.username === 'string' ? values.username : undefined,
|
|
120
|
+
typeof values.password === 'string' ? values.password : undefined);
|
|
121
|
+
const key = tasmotaCredentialKey(reference!);
|
|
122
|
+
let committed = false;
|
|
123
|
+
let write: Promise<void> = Promise.resolve();
|
|
124
|
+
leaseArg.own(async () => {
|
|
125
|
+
const result = await Promise.allSettled([write]);
|
|
126
|
+
if (result[0].status === 'fulfilled' && !committed) {
|
|
127
|
+
try { await store.delete(key); }
|
|
128
|
+
catch { throw new Error('Tasmota cancelled credential cleanup failed.'); }
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
write = Promise.resolve().then(() => store.create(key, credential));
|
|
132
|
+
try { await write; }
|
|
133
|
+
catch { throw new Error('Tasmota encrypted credential persistence failed.'); }
|
|
134
|
+
leaseArg.assertOpen();
|
|
135
|
+
committed = true;
|
|
136
|
+
} else if (!store && !this.hasResolver) throw new Error('Tasmota credential reference requires an explicit credential source.');
|
|
137
|
+
}
|
|
138
|
+
submitted = true;
|
|
139
|
+
return { kind: 'done', title: 'Selected Tasmota device configured', config };
|
|
140
|
+
});
|
|
78
141
|
} catch (errorArg) {
|
|
79
142
|
return { kind: 'error', error: errorArg instanceof Error ? errorArg.message : 'Invalid Tasmota configuration.' };
|
|
80
|
-
}
|
|
143
|
+
} finally { pending = false; }
|
|
81
144
|
},
|
|
82
145
|
};
|
|
83
146
|
}
|
|
@@ -50,9 +50,9 @@ export class TasmotaMapper {
|
|
|
50
50
|
return sensors;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
public static device(deviceArg: ITasmotaSelectedDevice, stateArg: ITasmotaDeviceState, connectedArg: boolean): plugins.shxInterfaces.data.IDeviceDefinition {
|
|
54
|
-
const features: plugins.shxInterfaces.data.IDeviceFeature[] = deviceArg.relays.map((relayArg) => ({ id: `power_${relayArg}`, capability: 'switch', name: `Relay ${relayArg}`, readable: true, writable:
|
|
55
|
-
if (deviceArg.brightness) features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable:
|
|
53
|
+
public static device(deviceArg: ITasmotaSelectedDevice, stateArg: ITasmotaDeviceState, connectedArg: boolean, enableControlsArg = false): plugins.shxInterfaces.data.IDeviceDefinition {
|
|
54
|
+
const features: plugins.shxInterfaces.data.IDeviceFeature[] = deviceArg.relays.map((relayArg) => ({ id: `power_${relayArg}`, capability: 'switch', name: `Relay ${relayArg}`, readable: true, writable: enableControlsArg }));
|
|
55
|
+
if (deviceArg.brightness) features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable: enableControlsArg, unit: '%' });
|
|
56
56
|
for (const id of Object.keys(stateArg.sensors).sort()) features.push({ id, capability: id.startsWith('sensor_ENERGY/') ? 'energy' : 'sensor', name: id.slice('sensor_'.length).split('/').map(decodeURIComponent).join(' '), readable: true, writable: false });
|
|
57
57
|
const values = { ...stateArg.controls, ...stateArg.sensors };
|
|
58
58
|
return {
|
|
@@ -69,12 +69,12 @@ export class TasmotaMapper {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
public static entities(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition): IIntegrationEntity[] {
|
|
72
|
+
public static entities(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition, homeIdArg: string): IIntegrationEntity[] {
|
|
73
73
|
return deviceArg.features.map((featureArg) => {
|
|
74
74
|
const platform = featureArg.capability === 'switch' ? 'switch' : featureArg.capability === 'light' ? 'light' : 'sensor';
|
|
75
75
|
return {
|
|
76
|
-
id: `${platform}.${encodeURIComponent(deviceArg.id
|
|
77
|
-
uniqueId:
|
|
76
|
+
id: `${encodeURIComponent(homeIdArg)}:${platform}.${encodeURIComponent(JSON.stringify([deviceArg.id, featureArg.id]))}`,
|
|
77
|
+
uniqueId: JSON.stringify([homeIdArg, deviceArg.id, featureArg.id]),
|
|
78
78
|
integrationDomain: 'tasmota',
|
|
79
79
|
deviceId: deviceArg.id,
|
|
80
80
|
platform,
|