@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,372 +1,92 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
IKodiDeviceInfo,
|
|
6
|
-
IKodiJsonRpcRequest,
|
|
7
|
-
IKodiJsonRpcResponse,
|
|
8
|
-
IKodiMediaItem,
|
|
9
|
-
IKodiPlayerProperties,
|
|
10
|
-
IKodiSnapshot,
|
|
11
|
-
IKodiTime,
|
|
12
|
-
TKodiInputCommand,
|
|
13
|
-
TKodiJsonRpcParams,
|
|
14
|
-
TKodiMediaType,
|
|
15
|
-
} from './kodi.types.js';
|
|
16
|
-
|
|
17
|
-
const defaultPort = 8080;
|
|
18
|
-
const defaultWsPort = 9090;
|
|
19
|
-
const defaultTimeoutMs = 5000;
|
|
20
|
-
|
|
21
|
-
const playerProperties = ['time', 'totaltime', 'speed', 'live', 'percentage', 'playlistid', 'position', 'repeat', 'shuffled'];
|
|
22
|
-
const itemProperties = ['title', 'file', 'uniqueid', 'thumbnail', 'artist', 'albumartist', 'showtitle', 'album', 'season', 'episode', 'streamdetails'];
|
|
23
|
-
|
|
24
|
-
export class KodiJsonRpcError extends Error {
|
|
25
|
-
constructor(public readonly method: string, public readonly code: number | undefined, messageArg: string, public readonly data?: unknown) {
|
|
26
|
-
super(`Kodi JSON-RPC ${method} failed${typeof code === 'number' ? ` (${code})` : ''}: ${messageArg}`);
|
|
27
|
-
this.name = 'KodiJsonRpcError';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { NativeHomeAdmission, type INativeAdmissionContext } from '../../core/classes.nativehomeadmission.js';
|
|
3
|
+
import type { IKodiClientOptions, IKodiConfig, IKodiNativeSnapshot, IKodiSetupContext } from './kodi.types.js';
|
|
4
|
+
import { kodiKeys, kodiRecord, kodiSignal, normalizeKodiConfig } from './kodi.validation.js';
|
|
30
5
|
|
|
6
|
+
/** Consumer lifecycle and home projection; the foundation owns all protocol work. */
|
|
31
7
|
export class KodiClient {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
]) : [undefined, undefined];
|
|
58
|
-
|
|
59
|
-
return this.normalizeSnapshot({
|
|
60
|
-
deviceInfo: {
|
|
61
|
-
...this.deviceInfoFromConfig(),
|
|
62
|
-
name: this.config.name || this.config.deviceInfo?.name || application?.name || this.config.host,
|
|
63
|
-
version: this.versionString(application),
|
|
64
|
-
},
|
|
65
|
-
application,
|
|
66
|
-
players,
|
|
67
|
-
player,
|
|
68
|
-
playerProperties: playerPropertiesResult,
|
|
69
|
-
item,
|
|
70
|
-
online: true,
|
|
71
|
-
updatedAt: new Date().toISOString(),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
public async ping(): Promise<boolean> {
|
|
76
|
-
const result = await this.callMethod<string>('JSONRPC.Ping');
|
|
77
|
-
return result === 'pong';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
public async getApplicationProperties(): Promise<IKodiApplicationProperties> {
|
|
81
|
-
return this.callMethod<IKodiApplicationProperties>('Application.GetProperties', {
|
|
82
|
-
properties: ['name', 'version', 'volume', 'muted'],
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public async getActivePlayers(): Promise<IKodiActivePlayer[]> {
|
|
87
|
-
return this.callMethod<IKodiActivePlayer[]>('Player.GetActivePlayers');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public async getPlayerProperties(playerIdArg: number): Promise<IKodiPlayerProperties> {
|
|
91
|
-
return this.callMethod<IKodiPlayerProperties>('Player.GetProperties', {
|
|
92
|
-
playerid: playerIdArg,
|
|
93
|
-
properties: playerProperties,
|
|
8
|
+
readonly #config: Readonly<IKodiConfig>;
|
|
9
|
+
readonly #context: INativeAdmissionContext;
|
|
10
|
+
readonly #protocol: plugins.ecobridgeDeviceManager.KodiProtocol;
|
|
11
|
+
readonly #lifetime = new AbortController();
|
|
12
|
+
readonly #parentSignal?: AbortSignal;
|
|
13
|
+
readonly #onParentAbort: () => void;
|
|
14
|
+
readonly #operations = new Set<Promise<unknown>>();
|
|
15
|
+
#closed = false;
|
|
16
|
+
#healthy = false;
|
|
17
|
+
#closePromise?: Promise<void>;
|
|
18
|
+
|
|
19
|
+
public constructor(config: IKodiConfig, context: IKodiSetupContext, options: IKodiClientOptions = {}) {
|
|
20
|
+
this.#config = normalizeKodiConfig(config);
|
|
21
|
+
this.#context = new NativeHomeAdmission('kodi').admit(context, this.#config.homeId);
|
|
22
|
+
const value = kodiRecord(options, 'client options');
|
|
23
|
+
kodiKeys(value, ['getBasicCredentials']);
|
|
24
|
+
this.#parentSignal = this.#context.abortSignal;
|
|
25
|
+
if (value.getBasicCredentials !== undefined && typeof value.getBasicCredentials !== 'function') throw new TypeError('Kodi Basic provider must be a function.');
|
|
26
|
+
this.#parentSignal?.throwIfAborted();
|
|
27
|
+
this.#protocol = new plugins.ecobridgeDeviceManager.KodiProtocol({
|
|
28
|
+
origin: this.#config.origin, expectedApplicationName: this.#config.expectedApplicationName,
|
|
29
|
+
expectedApplicationMajor: this.#config.expectedApplicationMajor, allowUnencryptedHttp: true,
|
|
30
|
+
control: this.#config.control, timeoutMs: this.#config.timeoutMs,
|
|
31
|
+
maxQueuedOperations: this.#config.maxQueuedOperations, maxResponseBytes: this.#config.maxResponseBytes,
|
|
32
|
+
getBasicCredentials: value.getBasicCredentials as IKodiClientOptions['getBasicCredentials'],
|
|
94
33
|
});
|
|
34
|
+
this.#onParentAbort = () => { void this.destroy().catch(() => {}); };
|
|
35
|
+
this.#parentSignal?.addEventListener('abort', this.#onParentAbort, { once: true });
|
|
36
|
+
if (this.#parentSignal?.aborted) this.#onParentAbort();
|
|
95
37
|
}
|
|
96
38
|
|
|
97
|
-
public
|
|
98
|
-
const result = await this.callMethod<{ item?: IKodiMediaItem }>('Player.GetItem', {
|
|
99
|
-
playerid: playerIdArg,
|
|
100
|
-
properties: itemProperties,
|
|
101
|
-
});
|
|
102
|
-
return result.item;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public async setVolumeLevel(volumeLevelArg: number): Promise<number> {
|
|
106
|
-
return this.callMethod<number>('Application.SetVolume', {
|
|
107
|
-
volume: Math.max(0, Math.min(100, Math.round(volumeLevelArg * 100))),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
public async stepVolume(directionArg: 'increment' | 'decrement'): Promise<number> {
|
|
112
|
-
return this.callMethod<number>('Application.SetVolume', { volume: directionArg });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
public async setMuted(mutedArg: boolean): Promise<boolean> {
|
|
116
|
-
return this.callMethod<boolean>('Application.SetMute', { mute: mutedArg });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
public async playPause(): Promise<unknown> {
|
|
120
|
-
return this.playState('toggle');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
public async play(): Promise<unknown> {
|
|
124
|
-
return this.playState(true);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
public async pause(): Promise<unknown> {
|
|
128
|
-
return this.playState(false);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
public async stop(): Promise<string> {
|
|
132
|
-
return this.callMethod<string>('Player.Stop', { playerid: await this.activePlayerId() });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
public async nextTrack(): Promise<string> {
|
|
136
|
-
return this.callMethod<string>('Player.GoTo', { playerid: await this.activePlayerId(), to: 'next' });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
public async previousTrack(): Promise<string> {
|
|
140
|
-
return this.callMethod<string>('Player.GoTo', { playerid: await this.activePlayerId(), to: 'previous' });
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
public async seek(positionSecondsArg: number): Promise<unknown> {
|
|
144
|
-
return this.callMethod('Player.Seek', {
|
|
145
|
-
playerid: await this.activePlayerId(),
|
|
146
|
-
value: { time: this.secondsToTime(positionSecondsArg) },
|
|
147
|
-
});
|
|
148
|
-
}
|
|
39
|
+
public isHealthy(): boolean { return this.#healthy && !this.#closed && !this.#protocol.isClosed; }
|
|
149
40
|
|
|
150
|
-
public
|
|
151
|
-
return this.
|
|
41
|
+
public getSnapshot(signal?: AbortSignal): Promise<IKodiNativeSnapshot> {
|
|
42
|
+
return this.run(async (signal) => this.project(await this.#protocol.readSnapshot({ signal })), signal);
|
|
152
43
|
}
|
|
153
44
|
|
|
154
|
-
public
|
|
155
|
-
return this.
|
|
45
|
+
public setVolume(volume: number, signal?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IKodiControlReceipt> {
|
|
46
|
+
return this.run((signal) => this.#protocol.setVolume(volume, { signal }), signal);
|
|
156
47
|
}
|
|
157
48
|
|
|
158
|
-
public
|
|
159
|
-
return this.
|
|
160
|
-
playlistid: playlistIdArg,
|
|
161
|
-
item: this.playlistItem(mediaTypeArg, mediaIdArg),
|
|
162
|
-
});
|
|
49
|
+
public setMuted(muted: boolean, signal?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IKodiControlReceipt> {
|
|
50
|
+
return this.run((signal) => this.#protocol.setMuted(muted, { signal }), signal);
|
|
163
51
|
}
|
|
164
52
|
|
|
165
|
-
public
|
|
166
|
-
return this
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
53
|
+
public destroy(): Promise<void> {
|
|
54
|
+
if (this.#closePromise) return this.#closePromise;
|
|
55
|
+
this.#closed = true;
|
|
56
|
+
this.#healthy = false;
|
|
57
|
+
this.#closePromise = Promise.resolve().then(async () => {
|
|
58
|
+
const [protocolCleanup] = await Promise.allSettled([this.#protocol.close(), ...this.#operations]);
|
|
59
|
+
// Operation failures belong to their callers; this close still joins every projection.
|
|
60
|
+
if (protocolCleanup.status === 'rejected') throw protocolCleanup.reason;
|
|
171
61
|
});
|
|
62
|
+
this.#parentSignal?.removeEventListener('abort', this.#onParentAbort);
|
|
63
|
+
this.#lifetime.abort(new Error('Kodi client is closed.'));
|
|
64
|
+
return this.#closePromise;
|
|
172
65
|
}
|
|
173
66
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return this.callMethod(method);
|
|
67
|
+
private project(state: plugins.ecobridgeDeviceManager.IKodiSnapshot): IKodiNativeSnapshot {
|
|
68
|
+
return Object.freeze({ ...state, homeId: this.#context.homeId, name: this.#config.name ?? state.applicationName, online: true });
|
|
177
69
|
}
|
|
178
70
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
public async callMethod<T = unknown>(methodArg: string, paramsArg?: TKodiJsonRpcParams): Promise<T> {
|
|
184
|
-
if (!this.config.host) {
|
|
185
|
-
throw new Error('Kodi host is required when snapshot data is not provided.');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const request: IKodiJsonRpcRequest = {
|
|
189
|
-
jsonrpc: '2.0',
|
|
190
|
-
method: methodArg,
|
|
191
|
-
id: this.requestId++,
|
|
192
|
-
};
|
|
193
|
-
if (paramsArg !== undefined) {
|
|
194
|
-
request.params = paramsArg;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const abortController = new AbortController();
|
|
198
|
-
const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
|
|
71
|
+
private run<TResult>(work: (signal: AbortSignal) => Promise<TResult>, callerSignal?: AbortSignal): Promise<TResult> {
|
|
72
|
+
if (this.#closed || this.#protocol.isClosed) return Promise.reject(new plugins.ecobridgeDeviceManager.KodiProtocolError('closed', 'Kodi client is terminally closed.'));
|
|
73
|
+
let signal: AbortSignal;
|
|
199
74
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
clearTimeout(timeout);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
public async destroy(): Promise<void> {}
|
|
221
|
-
|
|
222
|
-
private async playState(playArg: boolean | 'toggle'): Promise<unknown> {
|
|
223
|
-
return this.callMethod('Player.PlayPause', {
|
|
224
|
-
playerid: await this.activePlayerId(),
|
|
225
|
-
play: playArg,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private async activePlayerId(): Promise<number> {
|
|
230
|
-
const snapshot = await this.getSnapshot();
|
|
231
|
-
const playerId = snapshot.player?.playerid ?? snapshot.players[0]?.playerid;
|
|
232
|
-
if (typeof playerId !== 'number') {
|
|
233
|
-
throw new Error('Kodi has no active player.');
|
|
234
|
-
}
|
|
235
|
-
return playerId;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private mediaItem(mediaTypeArg: TKodiMediaType, mediaIdArg: string | number): Record<string, unknown> {
|
|
239
|
-
const type = mediaTypeArg.toLowerCase();
|
|
240
|
-
if (type === 'playlist') {
|
|
241
|
-
return { playlistid: Number(mediaIdArg) };
|
|
242
|
-
}
|
|
243
|
-
if (type === 'channel') {
|
|
244
|
-
return { channelid: Number(mediaIdArg) };
|
|
245
|
-
}
|
|
246
|
-
if (type === 'directory') {
|
|
247
|
-
return { path: String(mediaIdArg), recursive: true };
|
|
248
|
-
}
|
|
249
|
-
if (type === 'movie') {
|
|
250
|
-
return { movieid: Number(mediaIdArg) };
|
|
251
|
-
}
|
|
252
|
-
if (type === 'episode') {
|
|
253
|
-
return { episodeid: Number(mediaIdArg) };
|
|
254
|
-
}
|
|
255
|
-
if (type === 'season') {
|
|
256
|
-
return { seasonid: Number(mediaIdArg) };
|
|
257
|
-
}
|
|
258
|
-
if (type === 'tvshow') {
|
|
259
|
-
return { tvshowid: Number(mediaIdArg) };
|
|
260
|
-
}
|
|
261
|
-
if (type === 'album') {
|
|
262
|
-
return { albumid: Number(mediaIdArg) };
|
|
263
|
-
}
|
|
264
|
-
if (type === 'artist') {
|
|
265
|
-
return { artistid: Number(mediaIdArg) };
|
|
266
|
-
}
|
|
267
|
-
if (type === 'song' || type === 'track') {
|
|
268
|
-
return { songid: Number(mediaIdArg) };
|
|
269
|
-
}
|
|
270
|
-
return { file: String(mediaIdArg) };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
private playlistItem(mediaTypeArg: TKodiMediaType, mediaIdArg: string | number): Record<string, unknown> {
|
|
274
|
-
const type = mediaTypeArg.toLowerCase();
|
|
275
|
-
if (type === 'album') {
|
|
276
|
-
return { albumid: Number(mediaIdArg) };
|
|
277
|
-
}
|
|
278
|
-
if (type === 'artist') {
|
|
279
|
-
return { artistid: Number(mediaIdArg) };
|
|
280
|
-
}
|
|
281
|
-
if (type === 'song' || type === 'track') {
|
|
282
|
-
return { songid: Number(mediaIdArg) };
|
|
283
|
-
}
|
|
284
|
-
return this.mediaItem(type, mediaIdArg);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private secondsToTime(secondsArg: number): IKodiTime {
|
|
288
|
-
const safeSeconds = Math.max(0, Math.floor(secondsArg));
|
|
289
|
-
return {
|
|
290
|
-
hours: Math.floor(safeSeconds / 3600),
|
|
291
|
-
minutes: Math.floor((safeSeconds % 3600) / 60),
|
|
292
|
-
seconds: safeSeconds % 60,
|
|
293
|
-
milliseconds: Math.max(0, Math.round((secondsArg - Math.floor(secondsArg)) * 1000)),
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private inputMethod(commandArg: string): string {
|
|
298
|
-
const normalized = commandArg.replace(/[_\s-]+/g, '').toLowerCase();
|
|
299
|
-
const methods: Record<string, string> = {
|
|
300
|
-
up: 'Input.Up',
|
|
301
|
-
down: 'Input.Down',
|
|
302
|
-
left: 'Input.Left',
|
|
303
|
-
right: 'Input.Right',
|
|
304
|
-
select: 'Input.Select',
|
|
305
|
-
ok: 'Input.Select',
|
|
306
|
-
enter: 'Input.Select',
|
|
307
|
-
back: 'Input.Back',
|
|
308
|
-
home: 'Input.Home',
|
|
309
|
-
info: 'Input.Info',
|
|
310
|
-
contextmenu: 'Input.ContextMenu',
|
|
311
|
-
};
|
|
312
|
-
return methods[normalized] || (commandArg.includes('.') ? commandArg : `Input.${commandArg}`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private normalizeSnapshot(snapshotArg: IKodiSnapshot): IKodiSnapshot {
|
|
316
|
-
const deviceInfo = {
|
|
317
|
-
...this.deviceInfoFromConfig(),
|
|
318
|
-
...snapshotArg.deviceInfo,
|
|
319
|
-
};
|
|
320
|
-
if (!deviceInfo.name) {
|
|
321
|
-
deviceInfo.name = snapshotArg.application?.name || this.config.name || this.config.host || 'Kodi';
|
|
322
|
-
}
|
|
323
|
-
if (!deviceInfo.version) {
|
|
324
|
-
deviceInfo.version = this.versionString(snapshotArg.application);
|
|
325
|
-
}
|
|
326
|
-
return {
|
|
327
|
-
...snapshotArg,
|
|
328
|
-
deviceInfo,
|
|
329
|
-
players: snapshotArg.players || [],
|
|
330
|
-
player: snapshotArg.player || snapshotArg.players?.[0],
|
|
331
|
-
online: snapshotArg.online,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private deviceInfoFromConfig(): IKodiDeviceInfo {
|
|
336
|
-
return {
|
|
337
|
-
...this.config.deviceInfo,
|
|
338
|
-
id: this.config.deviceInfo?.id || this.config.uniqueId,
|
|
339
|
-
uuid: this.config.deviceInfo?.uuid || this.config.uniqueId,
|
|
340
|
-
name: this.config.deviceInfo?.name || this.config.name,
|
|
341
|
-
host: this.config.deviceInfo?.host || this.config.host,
|
|
342
|
-
port: this.config.deviceInfo?.port || this.config.port || defaultPort,
|
|
343
|
-
wsPort: this.config.deviceInfo?.wsPort || this.config.wsPort || defaultWsPort,
|
|
344
|
-
manufacturer: this.config.deviceInfo?.manufacturer || 'Kodi',
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private versionString(applicationArg: IKodiApplicationProperties | undefined): string | undefined {
|
|
349
|
-
const version = applicationArg?.version;
|
|
350
|
-
if (!version || typeof version.major !== 'number') {
|
|
351
|
-
return undefined;
|
|
352
|
-
}
|
|
353
|
-
return `${version.major}.${typeof version.minor === 'number' ? version.minor : 0}${version.revision ? `-${version.revision}` : ''}`;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private cloneSnapshot(snapshotArg: IKodiSnapshot): IKodiSnapshot {
|
|
357
|
-
return JSON.parse(JSON.stringify(snapshotArg)) as IKodiSnapshot;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
private jsonRpcUrl(): string {
|
|
361
|
-
const protocol = this.config.ssl ? 'https' : 'http';
|
|
362
|
-
return `${protocol}://${this.config.host}:${this.config.port || defaultPort}/jsonrpc`;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private headers(): Record<string, string> {
|
|
366
|
-
const headers: Record<string, string> = { 'content-type': 'application/json' };
|
|
367
|
-
if (this.config.username !== undefined && this.config.password !== undefined) {
|
|
368
|
-
headers.authorization = `Basic ${globalThis.btoa(`${this.config.username}:${this.config.password}`)}`;
|
|
369
|
-
}
|
|
370
|
-
return headers;
|
|
75
|
+
kodiSignal(callerSignal);
|
|
76
|
+
if (callerSignal?.aborted) throw new plugins.ecobridgeDeviceManager.KodiProtocolError('cancelled', 'Kodi operation was cancelled before admission.');
|
|
77
|
+
signal = AbortSignal.any([this.#lifetime.signal, ...(callerSignal ? [callerSignal] : [])]);
|
|
78
|
+
} catch (error) { return Promise.reject(error); }
|
|
79
|
+
let operation: Promise<TResult>;
|
|
80
|
+
try { operation = work(signal).then((result) => {
|
|
81
|
+
this.#healthy = !this.#closed && !signal.aborted && !this.#protocol.isClosed;
|
|
82
|
+
return result;
|
|
83
|
+
}, (error: unknown) => {
|
|
84
|
+
this.#healthy = false;
|
|
85
|
+
throw error;
|
|
86
|
+
}); } catch (error) { return Promise.reject(error); }
|
|
87
|
+
this.#operations.add(operation);
|
|
88
|
+
const settled = (): void => { this.#operations.delete(operation); };
|
|
89
|
+
void operation.then(settled, settled);
|
|
90
|
+
return operation;
|
|
371
91
|
}
|
|
372
92
|
}
|
|
@@ -1,63 +1,60 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { IConfigFlow, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
|
|
3
|
+
import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
|
|
4
|
+
import { KodiClient } from './kodi.classes.client.js';
|
|
5
|
+
import type { IKodiConfig, IKodiConfigFlowContext } from './kodi.types.js';
|
|
6
|
+
import { kodiInteger, kodiKeys, kodiRecord, kodiText, normalizeKodiConfig } from './kodi.validation.js';
|
|
7
7
|
|
|
8
|
+
/** Captures selected configuration. Setup separately owns live readiness admission. */
|
|
8
9
|
export class KodiConfigFlow implements IConfigFlow<IKodiConfig> {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
readonly #scope: NativeIntegrationScope;
|
|
11
|
+
public constructor(owner: NativeIntegrationOwner = new NativeIntegrationOwner('kodi')) {
|
|
12
|
+
this.#scope = owner.createFlowScope();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public destroy(): Promise<void> { return this.#scope.close(); }
|
|
16
|
+
public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
|
|
17
|
+
|
|
18
|
+
public async start(input: IDiscoveryCandidate, context: IKodiConfigFlowContext): Promise<IConfigFlowStep<IKodiConfig>> {
|
|
19
|
+
const candidate = kodiRecord(input, 'config candidate');
|
|
20
|
+
const host = kodiText(candidate.host, 'selected candidate host', 128);
|
|
21
|
+
const port = kodiInteger(candidate.port === undefined ? 8080 : candidate.port, 1, 65535, 'candidate port');
|
|
22
|
+
const origin = plugins.ecobridgeDeviceManager.validateKodiOrigin(`http://${host}:${port}`);
|
|
23
|
+
const captured = this.#scope.admit(context);
|
|
11
24
|
return {
|
|
12
|
-
kind: 'form',
|
|
13
|
-
|
|
14
|
-
description: 'Configure the local Kodi JSON-RPC endpoint.',
|
|
25
|
+
kind: 'form', title: 'Connect selected Kodi application',
|
|
26
|
+
description: 'Specify the expected application name and major for the host-selected home. Accept plaintext HTTP explicitly. Controls default to disabled; enabling them requires endpoint trust and an integer volume ceiling. Live readiness is checked during setup.',
|
|
15
27
|
fields: [
|
|
16
|
-
{ name: '
|
|
17
|
-
{ name: '
|
|
18
|
-
{ name: '
|
|
19
|
-
{ name: '
|
|
20
|
-
{ name: '
|
|
21
|
-
{ name: '
|
|
22
|
-
{ name: 'name', label: '
|
|
28
|
+
{ name: 'expectedApplicationName', label: 'Exact application name', type: 'text', required: true },
|
|
29
|
+
{ name: 'expectedApplicationMajor', label: 'Application major version', type: 'number', required: true },
|
|
30
|
+
{ name: 'allowUnencryptedHttp', label: 'Accept plaintext HTTP', type: 'boolean', required: true },
|
|
31
|
+
{ name: 'allowControls', label: 'Enable application volume and mute controls', type: 'boolean' },
|
|
32
|
+
{ name: 'trustSelectedEndpoint', label: 'Trust this selected endpoint for controls', type: 'boolean' },
|
|
33
|
+
{ name: 'maximumVolume', label: 'Maximum integer application volume (0–100)', type: 'number' },
|
|
34
|
+
{ name: 'name', label: 'Display name', type: 'text' },
|
|
23
35
|
],
|
|
24
|
-
submit: async (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
submit: async (input) => {
|
|
37
|
+
try {
|
|
38
|
+
const values = kodiRecord(input, 'config-flow values');
|
|
39
|
+
return await this.#scope.run(captured, captured.homeId, async (lease): Promise<IConfigFlowStep<IKodiConfig>> => {
|
|
40
|
+
kodiKeys(values, ['homeId', 'expectedApplicationName', 'expectedApplicationMajor', 'allowUnencryptedHttp', 'allowControls', 'trustSelectedEndpoint', 'maximumVolume', 'name', 'timeoutMs', 'maxQueuedOperations', 'maxResponseBytes']);
|
|
41
|
+
if (values.homeId !== undefined && values.homeId !== lease.context.homeId) throw new TypeError('Kodi form home differs from the host-selected home.');
|
|
42
|
+
if (values.allowControls !== undefined && typeof values.allowControls !== 'boolean') throw new TypeError('Kodi controls require an explicit boolean opt-in.');
|
|
43
|
+
if (values.allowControls !== true && (values.trustSelectedEndpoint === true || values.maximumVolume !== undefined)) throw new TypeError('Kodi owner control policy requires an explicit controls opt-in.');
|
|
44
|
+
const config = normalizeKodiConfig({ homeId: lease.context.homeId, origin,
|
|
45
|
+
expectedApplicationName: values.expectedApplicationName, expectedApplicationMajor: values.expectedApplicationMajor,
|
|
46
|
+
allowUnencryptedHttp: values.allowUnencryptedHttp,
|
|
47
|
+
control: values.allowControls === true ? { trustSelectedEndpoint: values.trustSelectedEndpoint, maximumVolume: values.maximumVolume } : undefined,
|
|
48
|
+
name: values.name, timeoutMs: values.timeoutMs, maxQueuedOperations: values.maxQueuedOperations, maxResponseBytes: values.maxResponseBytes });
|
|
49
|
+
// Construction delegates endpoint validation without opening a connection.
|
|
50
|
+
const client = new KodiClient(config, { ...lease.context, abortSignal: lease.signal });
|
|
51
|
+
lease.own(() => client.destroy());
|
|
52
|
+
return { kind: 'done', title: 'Kodi configuration captured', config };
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { kind: 'error', error: error instanceof Error ? error.message : 'Kodi configuration was refused.' };
|
|
56
|
+
}
|
|
57
|
+
},
|
|
39
58
|
};
|
|
40
59
|
}
|
|
41
|
-
|
|
42
|
-
private stringValue(valueArg: unknown): string | undefined {
|
|
43
|
-
return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private numberValue(valueArg: unknown): number | undefined {
|
|
47
|
-
return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private booleanValue(valueArg: unknown): boolean | undefined {
|
|
51
|
-
return typeof valueArg === 'boolean' ? valueArg : undefined;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private numberMetadata(candidateArg: IDiscoveryCandidate, keyArg: string): number | undefined {
|
|
55
|
-
const value = candidateArg.metadata?.[keyArg];
|
|
56
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private booleanMetadata(candidateArg: IDiscoveryCandidate, keyArg: string): boolean | undefined {
|
|
60
|
-
const value = candidateArg.metadata?.[keyArg];
|
|
61
|
-
return typeof value === 'boolean' ? value : undefined;
|
|
62
|
-
}
|
|
63
60
|
}
|