@smarthome.exchange/integrations 1.0.0 → 2.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 +49 -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 +79 -0
- package/dist_ts/core/classes.nativeintegrationowner.js +269 -0
- package/dist_ts/core/index.d.ts +2 -0
- package/dist_ts/core/index.js +3 -1
- package/dist_ts/core/types.d.ts +14 -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/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/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/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/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/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/package.json +3 -3
- package/readme.md +514 -2
- 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 +297 -0
- package/ts/core/index.ts +2 -0
- package/ts/core/types.ts +16 -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/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/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/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/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/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
|
@@ -1,55 +1,60 @@
|
|
|
1
|
+
import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
|
|
1
2
|
import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
3
|
+
import { SoundtouchClient } from './soundtouch.classes.client.js';
|
|
4
|
+
import { createSoundtouchDiscoveryDescriptor } from './soundtouch.discovery.js';
|
|
5
|
+
import type { ISoundtouchConfig, ISoundtouchHomeOptions } from './soundtouch.types.js';
|
|
6
|
+
import { normalizeSoundtouchConfig, soundtouchKeys, soundtouchRecord, soundtouchText } from './soundtouch.validation.js';
|
|
4
7
|
|
|
5
8
|
export class SoundtouchConfigFlow implements IConfigFlow<ISoundtouchConfig> {
|
|
9
|
+
readonly #owner: NativeIntegrationOwner;
|
|
10
|
+
readonly #scope: NativeIntegrationScope;
|
|
11
|
+
readonly #standalone: boolean;
|
|
12
|
+
public constructor(optionsArg: ISoundtouchHomeOptions | NativeIntegrationOwner = {}) {
|
|
13
|
+
this.#standalone = !(optionsArg instanceof NativeIntegrationOwner);
|
|
14
|
+
this.#owner = optionsArg instanceof NativeIntegrationOwner ? optionsArg : new NativeIntegrationOwner('soundtouch', optionsArg);
|
|
15
|
+
this.#scope = this.#owner.createFlowScope();
|
|
16
|
+
}
|
|
6
17
|
public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<ISoundtouchConfig>> {
|
|
7
|
-
|
|
18
|
+
// Capture host authority synchronously before the matcher or any submission can await.
|
|
19
|
+
const context = this.#scope.admit(contextArg);
|
|
20
|
+
const matched = await createSoundtouchDiscoveryDescriptor().getValidators()[0].validate(candidateArg, {});
|
|
21
|
+
if (!matched.matched || !matched.candidate) return { kind: 'error', error: matched.reason };
|
|
22
|
+
const candidate = matched.candidate;
|
|
8
23
|
return {
|
|
9
|
-
kind: 'form',
|
|
10
|
-
|
|
11
|
-
description: candidateArg.source === 'manual'
|
|
12
|
-
? 'Configure a local Bose SoundTouch speaker.'
|
|
13
|
-
: 'Confirm or adjust the discovered local Bose SoundTouch speaker.',
|
|
24
|
+
kind: 'form', title: 'Connect selected Bose SoundTouch speaker',
|
|
25
|
+
description: 'Acknowledge the plaintext LAN connection and choose an absolute integer volume ceiling. The host home and selected endpoint/deviceID/type stay fixed.',
|
|
14
26
|
fields: [
|
|
15
|
-
{ name: '
|
|
16
|
-
{ name: '
|
|
17
|
-
{ name: 'name', label: '
|
|
27
|
+
{ name: 'allowUnencryptedLan', label: 'Acknowledge unauthenticated plaintext LAN', type: 'boolean', required: true },
|
|
28
|
+
{ name: 'volumeLimit', label: 'Maximum absolute volume (0–100)', type: 'number', required: true },
|
|
29
|
+
{ name: 'name', label: 'Local display label', type: 'text' },
|
|
18
30
|
],
|
|
19
31
|
submit: async (valuesArg) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
try {
|
|
33
|
+
const values = soundtouchRecord(valuesArg, 'config flow values');
|
|
34
|
+
soundtouchKeys(values, ['homeId', 'allowUnencryptedLan', 'volumeLimit', 'name', 'requestTimeoutMs', 'maxPendingOperations', 'maxResponseBytes']);
|
|
35
|
+
if (values.homeId !== undefined && soundtouchText(values.homeId, 'submitted homeId', 256) !== context.homeId) {
|
|
36
|
+
throw new TypeError('SoundTouch submitted home conflicts with the captured host home.');
|
|
37
|
+
}
|
|
38
|
+
const config = normalizeSoundtouchConfig({ ...values, homeId: context.homeId,
|
|
39
|
+
host: candidate.host, port: candidate.port, deviceId: candidate.id, type: candidate.metadata?.type });
|
|
40
|
+
return await this.#scope.run(context, config.homeId, async (leaseArg) => {
|
|
41
|
+
const client = new SoundtouchClient(config, { ...leaseArg.context, abortSignal: leaseArg.signal });
|
|
42
|
+
let cleanupAttempted = false;
|
|
43
|
+
leaseArg.own(async () => {
|
|
44
|
+
if (cleanupAttempted) return client.retryCleanup();
|
|
45
|
+
cleanupAttempted = true;
|
|
46
|
+
await client.destroy();
|
|
47
|
+
});
|
|
48
|
+
await client.getSnapshot(leaseArg.signal);
|
|
49
|
+
leaseArg.assertOpen();
|
|
50
|
+
return { kind: 'done' as const, title: 'Selected SoundTouch speaker verified', config };
|
|
51
|
+
});
|
|
52
|
+
} catch (errorArg) {
|
|
53
|
+
return { kind: 'error', error: errorArg instanceof Error ? errorArg.message : 'SoundTouch selected setup failed.' };
|
|
23
54
|
}
|
|
24
|
-
return {
|
|
25
|
-
kind: 'done',
|
|
26
|
-
title: 'Bose SoundTouch configured',
|
|
27
|
-
config: {
|
|
28
|
-
host,
|
|
29
|
-
port: numberValue(valuesArg.port) || candidateArg.port || soundtouchDefaultPort,
|
|
30
|
-
dlnaPort: soundtouchDefaultDlnaPort,
|
|
31
|
-
name: stringValue(valuesArg.name) || candidateArg.name,
|
|
32
|
-
deviceId: candidateArg.id,
|
|
33
|
-
model: candidateArg.model,
|
|
34
|
-
macAddress: candidateArg.macAddress,
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
55
|
},
|
|
38
56
|
};
|
|
39
57
|
}
|
|
58
|
+
public destroy(): Promise<void> { return this.#standalone ? this.#owner.close() : this.#scope.close(); }
|
|
59
|
+
public retryCleanup(): Promise<void> { return this.#standalone ? this.#owner.retryCleanup() : this.#scope.retryCleanup(); }
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
const stringValue = (valueArg: unknown): string | undefined => {
|
|
43
|
-
return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const numberValue = (valueArg: unknown): number | undefined => {
|
|
47
|
-
if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
|
|
48
|
-
return valueArg;
|
|
49
|
-
}
|
|
50
|
-
if (typeof valueArg === 'string' && valueArg.trim()) {
|
|
51
|
-
const parsed = Number(valueArg);
|
|
52
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
};
|
|
@@ -1,178 +1,123 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
|
3
|
-
import
|
|
3
|
+
import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
|
|
4
|
+
import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
|
|
4
5
|
import { SoundtouchClient } from './soundtouch.classes.client.js';
|
|
5
6
|
import { SoundtouchConfigFlow } from './soundtouch.classes.configflow.js';
|
|
6
7
|
import { createSoundtouchDiscoveryDescriptor } from './soundtouch.discovery.js';
|
|
7
8
|
import { SoundtouchMapper } from './soundtouch.mapper.js';
|
|
8
|
-
import type {
|
|
9
|
+
import type { ISoundtouchConfig, ISoundtouchHomeOptions, ISoundtouchSnapshot } from './soundtouch.types.js';
|
|
10
|
+
import { soundtouchProfile } from './soundtouch.types.js';
|
|
11
|
+
import { normalizeSoundtouchConfig, soundtouchBoolean, soundtouchInteger, soundtouchKeys, soundtouchRecord } from './soundtouch.validation.js';
|
|
9
12
|
|
|
10
13
|
export class SoundtouchIntegration extends BaseIntegration<ISoundtouchConfig> {
|
|
11
14
|
public readonly domain = 'soundtouch';
|
|
12
15
|
public readonly displayName = 'Bose SoundTouch';
|
|
13
16
|
public readonly status = 'control-runtime' as const;
|
|
17
|
+
public readonly metadata = soundtouchProfile.metadata;
|
|
14
18
|
public readonly discoveryDescriptor = createSoundtouchDiscoveryDescriptor();
|
|
15
|
-
public readonly configFlow
|
|
16
|
-
|
|
17
|
-
source: 'home-assistant/core',
|
|
18
|
-
upstreamPath: 'homeassistant/components/soundtouch',
|
|
19
|
-
upstreamDomain: 'soundtouch',
|
|
20
|
-
integrationType: 'device',
|
|
21
|
-
iotClass: 'local_polling',
|
|
22
|
-
requirements: ['libsoundtouch==0.8'],
|
|
23
|
-
dependencies: [],
|
|
24
|
-
afterDependencies: [],
|
|
25
|
-
codeowners: ['@kroimon'],
|
|
26
|
-
configFlow: true,
|
|
27
|
-
documentation: 'https://www.home-assistant.io/integrations/soundtouch',
|
|
28
|
-
zeroconf: ['_soundtouch._tcp.local.'],
|
|
29
|
-
};
|
|
19
|
+
public readonly configFlow: SoundtouchConfigFlow;
|
|
20
|
+
readonly #owner: NativeIntegrationOwner;
|
|
30
21
|
|
|
31
|
-
public
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
public constructor(optionsArg: ISoundtouchHomeOptions = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this.#owner = new NativeIntegrationOwner('soundtouch', optionsArg);
|
|
25
|
+
this.configFlow = new SoundtouchConfigFlow(this.#owner);
|
|
34
26
|
}
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
public setup(configArg: ISoundtouchConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
|
|
28
|
+
const config = normalizeSoundtouchConfig(configArg);
|
|
29
|
+
return this.#owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
|
|
30
|
+
const client = new SoundtouchClient(config, { ...leaseArg.context, abortSignal: leaseArg.signal });
|
|
31
|
+
let cleanupAttempted = false;
|
|
32
|
+
leaseArg.own(async () => {
|
|
33
|
+
if (cleanupAttempted) return client.retryCleanup();
|
|
34
|
+
cleanupAttempted = true;
|
|
35
|
+
await client.destroy();
|
|
36
|
+
});
|
|
37
|
+
const snapshot = await client.getSnapshot(leaseArg.signal);
|
|
38
|
+
leaseArg.assertOpen();
|
|
39
|
+
const runtime = new SoundtouchRuntime(client, snapshot, leaseArg);
|
|
40
|
+
leaseArg.promoteToRuntime();
|
|
41
|
+
return runtime;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
public destroy(): Promise<void> { return this.#owner.close(); }
|
|
45
|
+
public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
|
|
37
46
|
}
|
|
38
|
-
|
|
39
47
|
export class HomeAssistantSoundtouchIntegration extends SoundtouchIntegration {}
|
|
40
48
|
|
|
41
49
|
class SoundtouchRuntime implements IIntegrationRuntime {
|
|
42
|
-
public domain = 'soundtouch';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
public readonly domain = 'soundtouch';
|
|
51
|
+
readonly #deviceId: string;
|
|
52
|
+
readonly #entityId: string;
|
|
53
|
+
constructor(private readonly client: SoundtouchClient, snapshotArg: ISoundtouchSnapshot, private readonly lease: INativeSetupLease) {
|
|
54
|
+
this.#deviceId = SoundtouchMapper.deviceId(snapshotArg);
|
|
55
|
+
this.#entityId = SoundtouchMapper.mediaEntityId(snapshotArg);
|
|
56
|
+
}
|
|
57
|
+
public isHealthy(): boolean { return !this.lease.signal.aborted && this.client.isHealthy(); }
|
|
58
|
+
public async devices(signalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
|
|
59
|
+
this.lease.assertOpen(); return SoundtouchMapper.toDevices(await this.client.getSnapshot(signalArg));
|
|
48
60
|
}
|
|
49
|
-
|
|
50
61
|
public async entities(): Promise<IIntegrationEntity[]> {
|
|
51
|
-
return SoundtouchMapper.toEntities(await this.client.getSnapshot());
|
|
62
|
+
this.lease.assertOpen(); return SoundtouchMapper.toEntities(await this.client.getSnapshot());
|
|
63
|
+
}
|
|
64
|
+
public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, signalArg?: AbortSignal): Promise<() => Promise<void>> {
|
|
65
|
+
this.lease.assertOpen();
|
|
66
|
+
if (typeof handlerArg !== 'function') throw new TypeError('SoundTouch runtime subscriber must be a function.');
|
|
67
|
+
const unsubscribe = this.client.subscribe((eventArg) => {
|
|
68
|
+
handlerArg({ type: eventArg.type === 'error' ? 'error' : 'state_changed', integrationDomain: 'soundtouch',
|
|
69
|
+
deviceId: this.#deviceId, entityId: this.#entityId, timestamp: Date.now(),
|
|
70
|
+
data: eventArg.type === 'snapshot' ? { snapshot: eventArg.snapshot, entity: SoundtouchMapper.toEntities(eventArg.snapshot)[0] }
|
|
71
|
+
: eventArg.type === 'control' ? { observation: eventArg.observation, effectOutcome: 'indeterminate' }
|
|
72
|
+
: { message: eventArg.error.message } });
|
|
73
|
+
}, signalArg);
|
|
74
|
+
return async () => unsubscribe();
|
|
52
75
|
}
|
|
53
|
-
|
|
54
76
|
public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
|
|
55
77
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
this.lease.assertOpen();
|
|
79
|
+
const request = soundtouchRecord(requestArg, 'service request');
|
|
80
|
+
soundtouchKeys(request, ['domain', 'service', 'target', 'data']);
|
|
81
|
+
if (typeof request.domain !== 'string' || typeof request.service !== 'string') throw new TypeError('SoundTouch service domain and name must be strings.');
|
|
82
|
+
const target = soundtouchRecord(request.target, 'service target'); soundtouchKeys(target, ['deviceId', 'entityId']);
|
|
83
|
+
if (target.deviceId === undefined && target.entityId === undefined
|
|
84
|
+
|| target.deviceId !== undefined && target.deviceId !== this.#deviceId
|
|
85
|
+
|| target.entityId !== undefined && target.entityId !== this.#entityId) throw new TypeError('SoundTouch services require an exact owned home/device or speaker entity target.');
|
|
86
|
+
const data = request.data === undefined ? {} : soundtouchRecord(request.data, 'service data');
|
|
87
|
+
if (request.domain === 'soundtouch' && ['status', 'snapshot', 'refresh'].includes(request.service)) {
|
|
88
|
+
soundtouchKeys(data, []); return { success: true, data: await this.client.getSnapshot() };
|
|
58
89
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
90
|
+
if (request.domain !== 'soundtouch' && request.domain !== 'media_player') throw new TypeError('SoundTouch service domain is unsupported.');
|
|
91
|
+
if (request.service === 'set_volume' || request.service === 'volume_set') {
|
|
92
|
+
soundtouchKeys(data, ['volume']); return observed(await this.client.setVolume(soundtouchInteger(data.volume, 0, 100, 'absolute volume')));
|
|
61
93
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
public async destroy(): Promise<void> {
|
|
69
|
-
await this.client.destroy();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private async callMediaPlayerService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
|
|
73
|
-
const command = this.commandFromMediaService(requestArg.service);
|
|
74
|
-
if (!command) {
|
|
75
|
-
return { success: false, error: `Unsupported SoundTouch media_player service: ${requestArg.service}` };
|
|
76
|
-
}
|
|
77
|
-
const result = await this.client.execute(this.commandRequest(command, requestArg));
|
|
78
|
-
return { success: true, data: result };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private async callSoundtouchService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
|
|
82
|
-
if (requestArg.service === 'play_preset' || requestArg.service === 'select_preset') {
|
|
83
|
-
const presetId = this.stringData(requestArg, 'preset_id') || this.stringData(requestArg, 'preset') || this.stringData(requestArg, 'presetId') || this.numberStringData(requestArg, 'preset');
|
|
84
|
-
const presetName = this.stringData(requestArg, 'preset_name') || this.stringData(requestArg, 'presetName') || this.stringData(requestArg, 'name');
|
|
85
|
-
const result = await this.client.execute({ command: 'play_preset', presetId, presetName });
|
|
86
|
-
return { success: true, data: result };
|
|
87
|
-
}
|
|
88
|
-
if (requestArg.service === 'play' || requestArg.service === 'pause' || requestArg.service === 'media_play' || requestArg.service === 'media_pause' || requestArg.service === 'volume_set' || requestArg.service === 'volume_mute' || requestArg.service === 'select_source' || requestArg.service === 'play_media') {
|
|
89
|
-
return this.callMediaPlayerService({ ...requestArg, domain: 'media_player' });
|
|
90
|
-
}
|
|
91
|
-
return { success: false, error: `Unsupported SoundTouch service: ${requestArg.service}` };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private commandFromMediaService(serviceArg: string): TSoundtouchCommand | undefined {
|
|
95
|
-
if (serviceArg === 'turn_on') {
|
|
96
|
-
return 'turn_on';
|
|
97
|
-
}
|
|
98
|
-
if (serviceArg === 'turn_off') {
|
|
99
|
-
return 'turn_off';
|
|
100
|
-
}
|
|
101
|
-
if (serviceArg === 'media_play' || serviceArg === 'play') {
|
|
102
|
-
return 'play';
|
|
103
|
-
}
|
|
104
|
-
if (serviceArg === 'media_pause' || serviceArg === 'pause') {
|
|
105
|
-
return 'pause';
|
|
106
|
-
}
|
|
107
|
-
if (serviceArg === 'media_play_pause' || serviceArg === 'play_pause') {
|
|
108
|
-
return 'play_pause';
|
|
109
|
-
}
|
|
110
|
-
if (serviceArg === 'media_previous_track' || serviceArg === 'previous_track') {
|
|
111
|
-
return 'previous_track';
|
|
112
|
-
}
|
|
113
|
-
if (serviceArg === 'media_next_track' || serviceArg === 'next_track') {
|
|
114
|
-
return 'next_track';
|
|
115
|
-
}
|
|
116
|
-
if (serviceArg === 'volume_up') {
|
|
117
|
-
return 'volume_up';
|
|
118
|
-
}
|
|
119
|
-
if (serviceArg === 'volume_down') {
|
|
120
|
-
return 'volume_down';
|
|
121
|
-
}
|
|
122
|
-
if (serviceArg === 'volume_set' || serviceArg === 'set_volume') {
|
|
123
|
-
return 'set_volume';
|
|
124
|
-
}
|
|
125
|
-
if (serviceArg === 'volume_mute' || serviceArg === 'mute') {
|
|
126
|
-
return 'mute';
|
|
127
|
-
}
|
|
128
|
-
if (serviceArg === 'select_source' || serviceArg === 'select_input') {
|
|
129
|
-
return 'select_source';
|
|
130
|
-
}
|
|
131
|
-
if (serviceArg === 'play_media') {
|
|
132
|
-
return 'play_media';
|
|
133
|
-
}
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private commandRequest(commandArg: TSoundtouchCommand, requestArg: IServiceCallRequest): ISoundtouchCommandRequest {
|
|
138
|
-
const request: ISoundtouchCommandRequest = { command: commandArg };
|
|
139
|
-
if (commandArg === 'set_volume') {
|
|
140
|
-
request.volumeLevel = this.numberData(requestArg, 'volume_level') ?? this.numberData(requestArg, 'volumeLevel');
|
|
141
|
-
request.volume = this.numberData(requestArg, 'volume');
|
|
142
|
-
}
|
|
143
|
-
if (commandArg === 'mute') {
|
|
144
|
-
request.muted = this.boolData(requestArg, 'is_volume_muted') ?? this.boolData(requestArg, 'mute') ?? this.boolData(requestArg, 'muted');
|
|
145
|
-
}
|
|
146
|
-
if (commandArg === 'select_source') {
|
|
147
|
-
request.source = this.stringData(requestArg, 'source') || this.stringData(requestArg, 'input');
|
|
148
|
-
}
|
|
149
|
-
if (commandArg === 'play_media') {
|
|
150
|
-
request.mediaId = this.stringData(requestArg, 'media_content_id') || this.stringData(requestArg, 'mediaId') || this.stringData(requestArg, 'uri') || this.numberStringData(requestArg, 'media_content_id');
|
|
151
|
-
request.mediaType = this.stringData(requestArg, 'media_content_type') || this.stringData(requestArg, 'mediaType');
|
|
152
|
-
request.url = this.stringData(requestArg, 'url');
|
|
153
|
-
request.presetId = this.stringData(requestArg, 'preset_id') || this.stringData(requestArg, 'presetId') || this.numberStringData(requestArg, 'preset');
|
|
154
|
-
request.presetName = this.stringData(requestArg, 'preset_name') || this.stringData(requestArg, 'presetName');
|
|
155
|
-
}
|
|
156
|
-
return request;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
|
|
160
|
-
const value = requestArg.data?.[keyArg];
|
|
161
|
-
return typeof value === 'string' && value ? value : undefined;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private numberData(requestArg: IServiceCallRequest, keyArg: string): number | undefined {
|
|
165
|
-
const value = requestArg.data?.[keyArg];
|
|
166
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
94
|
+
if (request.service === 'set_mute' || request.service === 'volume_mute') {
|
|
95
|
+
soundtouchKeys(data, ['muted']); return observed(await this.client.setMuted(soundtouchBoolean(data.muted)));
|
|
96
|
+
}
|
|
97
|
+
throw new TypeError('SoundTouch supports status reads and explicit absolute volume/mute only.');
|
|
98
|
+
} catch (errorArg) { return failed(errorArg); }
|
|
167
99
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
100
|
+
public async writeFeature(requestArg: IDeviceFeatureWriteRequest, signalArg?: AbortSignal): Promise<IServiceCallResult> {
|
|
101
|
+
try {
|
|
102
|
+
this.lease.assertOpen();
|
|
103
|
+
const request = soundtouchRecord(requestArg, 'feature write'); soundtouchKeys(request, ['device', 'feature', 'value']);
|
|
104
|
+
const device = soundtouchRecord(request.device, 'feature device');
|
|
105
|
+
const feature = soundtouchRecord(request.feature, 'feature');
|
|
106
|
+
if (device.id !== this.#deviceId || device.integrationDomain !== 'soundtouch') throw new TypeError('SoundTouch feature device is outside this home runtime.');
|
|
107
|
+
if ((feature.id !== 'volume' && feature.id !== 'mute') || feature.capability !== 'media') throw new TypeError('SoundTouch feature is not an admitted absolute media control.');
|
|
108
|
+
const value = feature.id === 'volume' ? soundtouchInteger(request.value, 0, 100, 'absolute volume') : soundtouchBoolean(request.value);
|
|
109
|
+
return observed(await this.client.writeFeature(feature.id, value, signalArg));
|
|
110
|
+
} catch (errorArg) { return failed(errorArg); }
|
|
172
111
|
}
|
|
112
|
+
public destroy(): Promise<void> { return this.lease.close(); }
|
|
113
|
+
}
|
|
173
114
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
115
|
+
function observed(observationArg: plugins.ecobridgeDeviceManager.IBoseSoundTouchControlResult): IServiceCallResult {
|
|
116
|
+
return { success: false, data: { ...observationArg, effectOutcome: 'indeterminate' } };
|
|
117
|
+
}
|
|
118
|
+
function failed(errorArg: unknown): IServiceCallResult {
|
|
119
|
+
return { success: false, error: errorArg instanceof Error ? errorArg.message : 'SoundTouch operation failed.', data: {
|
|
120
|
+
effectOutcome: errorArg instanceof plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError ? errorArg.effectOutcome : 'not-sent',
|
|
121
|
+
code: errorArg instanceof plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError ? errorArg.code : 'INVALID_OPERATION',
|
|
122
|
+
} };
|
|
178
123
|
}
|
|
@@ -1,137 +1,25 @@
|
|
|
1
1
|
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
2
|
-
import type { IDiscoveryCandidate,
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
public id = 'soundtouch-mdns-match';
|
|
11
|
-
public source = 'mdns' as const;
|
|
12
|
-
public description = 'Recognize Bose SoundTouch zeroconf advertisements.';
|
|
13
|
-
|
|
14
|
-
public async matches(recordArg: ISoundtouchMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
|
|
15
|
-
void contextArg;
|
|
16
|
-
const type = recordArg.type || '';
|
|
17
|
-
const txt = recordArg.txt || {};
|
|
18
|
-
const matched = normalizeMdnsType(type) === normalizeMdnsType(soundtouchMdnsType);
|
|
19
|
-
if (!matched) {
|
|
20
|
-
return { matched: false, confidence: 'low', reason: 'mDNS record is not a SoundTouch advertisement.' };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const id = valueForKey(txt, 'deviceId') || valueForKey(txt, 'deviceID') || valueForKey(txt, 'id') || recordArg.name;
|
|
24
|
-
return {
|
|
25
|
-
matched: true,
|
|
26
|
-
confidence: recordArg.host ? 'certain' : 'high',
|
|
27
|
-
reason: 'mDNS record matches Bose SoundTouch zeroconf metadata.',
|
|
28
|
-
normalizedDeviceId: id,
|
|
29
|
-
candidate: {
|
|
30
|
-
source: 'mdns',
|
|
31
|
-
integrationDomain: soundtouchDomain,
|
|
32
|
-
id,
|
|
33
|
-
host: recordArg.host,
|
|
34
|
-
port: recordArg.port || soundtouchDefaultPort,
|
|
35
|
-
name: recordArg.name,
|
|
36
|
-
manufacturer: 'Bose Corporation',
|
|
37
|
-
model: valueForKey(txt, 'type') || valueForKey(txt, 'model'),
|
|
38
|
-
macAddress: valueForKey(txt, 'mac') || valueForKey(txt, 'macAddress'),
|
|
39
|
-
metadata: { mdnsName: recordArg.name, mdnsType: type, txt },
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class SoundtouchManualMatcher implements IDiscoveryMatcher<ISoundtouchManualEntry> {
|
|
46
|
-
public id = 'soundtouch-manual-match';
|
|
47
|
-
public source = 'manual' as const;
|
|
48
|
-
public description = 'Recognize manual Bose SoundTouch setup entries.';
|
|
49
|
-
|
|
50
|
-
public async matches(inputArg: ISoundtouchManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
|
|
51
|
-
void contextArg;
|
|
52
|
-
const haystack = `${inputArg.name || ''} ${inputArg.manufacturer || ''} ${inputArg.model || ''}`.toLowerCase();
|
|
53
|
-
const matched = Boolean(inputArg.host || inputArg.metadata?.soundtouch || haystack.includes('soundtouch') || haystack.includes('bose'));
|
|
54
|
-
if (!matched) {
|
|
55
|
-
return { matched: false, confidence: 'low', reason: 'Manual entry does not contain SoundTouch setup hints.' };
|
|
2
|
+
import type { IDiscoveryCandidate, IDiscoveryMatch } from '../../core/types.js';
|
|
3
|
+
import { readSoundtouchEndpoint, soundtouchKeys, soundtouchRecord, soundtouchText } from './soundtouch.validation.js';
|
|
4
|
+
|
|
5
|
+
function matchSoundtouchCandidate(valueArg: unknown): IDiscoveryMatch {
|
|
6
|
+
try {
|
|
7
|
+
const input = soundtouchRecord(valueArg, 'selected candidate'); soundtouchKeys(input, ['source', 'integrationDomain', 'id', 'host', 'port', 'name', 'metadata']);
|
|
8
|
+
if (input.integrationDomain !== 'soundtouch' || input.source !== 'manual' && input.source !== 'custom') {
|
|
9
|
+
return { matched: false, confidence: 'low', reason: 'SoundTouch requires an explicitly selected manual/custom candidate.' };
|
|
56
10
|
}
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
candidate: {
|
|
65
|
-
source: 'manual',
|
|
66
|
-
integrationDomain: soundtouchDomain,
|
|
67
|
-
id,
|
|
68
|
-
host: inputArg.host,
|
|
69
|
-
port: inputArg.port || soundtouchDefaultPort,
|
|
70
|
-
name: inputArg.name,
|
|
71
|
-
manufacturer: normalizedManufacturer(inputArg.manufacturer),
|
|
72
|
-
model: inputArg.model,
|
|
73
|
-
macAddress: inputArg.macAddress,
|
|
74
|
-
metadata: inputArg.metadata,
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
11
|
+
const metadata = soundtouchRecord(input.metadata, 'candidate metadata'); soundtouchKeys(metadata, ['type']);
|
|
12
|
+
const endpoint = readSoundtouchEndpoint(input.host, input.id, metadata.type, input.port);
|
|
13
|
+
const candidate: IDiscoveryCandidate = Object.freeze({ source: input.source, integrationDomain: 'soundtouch', id: endpoint.deviceId,
|
|
14
|
+
host: endpoint.host, port: endpoint.port, ...(input.name === undefined ? {} : { name: soundtouchText(input.name, 'candidate name') }),
|
|
15
|
+
metadata: Object.freeze({ type: endpoint.type }) });
|
|
16
|
+
return { matched: true, confidence: 'medium', reason: 'Selected endpoint/deviceID/type; live identity is verified during setup.', candidate, normalizedDeviceId: endpoint.deviceId };
|
|
17
|
+
} catch (errorArg) { return { matched: false, confidence: 'low', reason: errorArg instanceof Error ? errorArg.message : 'Invalid SoundTouch candidate.' }; }
|
|
78
18
|
}
|
|
79
|
-
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
void contextArg;
|
|
86
|
-
const metadata = candidateArg.metadata || {};
|
|
87
|
-
const matched = candidateArg.integrationDomain === soundtouchDomain
|
|
88
|
-
|| includesSoundtouch(candidateArg.name)
|
|
89
|
-
|| includesSoundtouch(candidateArg.model)
|
|
90
|
-
|| includesBose(candidateArg.manufacturer)
|
|
91
|
-
|| Boolean(metadata.soundtouch)
|
|
92
|
-
|| normalizeMdnsType(stringMetadata(metadata.mdnsType) || '') === normalizeMdnsType(soundtouchMdnsType);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
matched,
|
|
96
|
-
confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
|
|
97
|
-
reason: matched ? 'Candidate has Bose SoundTouch metadata.' : 'Candidate is not SoundTouch.',
|
|
98
|
-
candidate: matched ? { ...candidateArg, port: candidateArg.port || soundtouchDefaultPort } : undefined,
|
|
99
|
-
normalizedDeviceId: candidateArg.id || candidateArg.macAddress,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
19
|
+
/** Matching only; this subset performs no network discovery. */
|
|
20
|
+
export function createSoundtouchDiscoveryDescriptor(): DiscoveryDescriptor {
|
|
21
|
+
return new DiscoveryDescriptor({ integrationDomain: 'soundtouch', displayName: 'Bose SoundTouch' })
|
|
22
|
+
.addMatcher({ id: 'soundtouch-manual-match', source: 'manual', matches: async (inputArg) => matchSoundtouchCandidate(inputArg) })
|
|
23
|
+
.addMatcher({ id: 'soundtouch-custom-match', source: 'custom', matches: async (inputArg) => matchSoundtouchCandidate(inputArg) })
|
|
24
|
+
.addValidator({ id: 'soundtouch-selected-endpoint', validate: async (candidateArg) => matchSoundtouchCandidate(candidateArg) });
|
|
102
25
|
}
|
|
103
|
-
|
|
104
|
-
export const createSoundtouchDiscoveryDescriptor = (): DiscoveryDescriptor => {
|
|
105
|
-
return new DiscoveryDescriptor({ integrationDomain: soundtouchDomain, displayName: 'Bose SoundTouch' })
|
|
106
|
-
.addMatcher(new SoundtouchMdnsMatcher())
|
|
107
|
-
.addMatcher(new SoundtouchManualMatcher())
|
|
108
|
-
.addValidator(new SoundtouchCandidateValidator());
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const valueForKey = (recordArg: Record<string, string | undefined> | undefined, keyArg: string): string | undefined => {
|
|
112
|
-
if (!recordArg) {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
const lowerKey = keyArg.toLowerCase();
|
|
116
|
-
for (const [key, value] of Object.entries(recordArg)) {
|
|
117
|
-
if (key.toLowerCase() === lowerKey) {
|
|
118
|
-
return value;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return undefined;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const normalizeMdnsType = (valueArg: string): string => valueArg.toLowerCase().replace(/\.$/, '');
|
|
125
|
-
|
|
126
|
-
const normalizedManufacturer = (valueArg: string | undefined): string => {
|
|
127
|
-
if (!valueArg || valueArg.toLowerCase().includes('bose')) {
|
|
128
|
-
return 'Bose Corporation';
|
|
129
|
-
}
|
|
130
|
-
return valueArg;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const includesSoundtouch = (valueArg: string | undefined): boolean => Boolean(valueArg?.toLowerCase().includes('soundtouch'));
|
|
134
|
-
|
|
135
|
-
const includesBose = (valueArg: string | undefined): boolean => Boolean(valueArg?.toLowerCase().includes('bose'));
|
|
136
|
-
|
|
137
|
-
const stringMetadata = (valueArg: unknown): string | undefined => typeof valueArg === 'string' ? valueArg : undefined;
|