@smarthome.exchange/integrations 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/changelog.md +35 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/classes.nativeintegrationowner.d.ts +2 -0
  4. package/dist_ts/core/classes.nativeintegrationowner.js +5 -1
  5. package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
  6. package/dist_ts/core/classes.runtimemanager.js +11 -3
  7. package/dist_ts/core/types.d.ts +2 -0
  8. package/dist_ts/integrations/deconz/deconz.classes.client.d.ts +37 -29
  9. package/dist_ts/integrations/deconz/deconz.classes.client.js +521 -294
  10. package/dist_ts/integrations/deconz/deconz.classes.configflow.d.ts +6 -1
  11. package/dist_ts/integrations/deconz/deconz.classes.configflow.js +63 -33
  12. package/dist_ts/integrations/deconz/deconz.classes.integration.d.ts +4 -3
  13. package/dist_ts/integrations/deconz/deconz.classes.integration.js +85 -40
  14. package/dist_ts/integrations/deconz/deconz.mapper.d.ts +1 -0
  15. package/dist_ts/integrations/deconz/deconz.mapper.js +24 -6
  16. package/dist_ts/integrations/deconz/deconz.types.d.ts +43 -6
  17. package/dist_ts/integrations/deconz/deconz.validation.d.ts +7 -1
  18. package/dist_ts/integrations/deconz/deconz.validation.js +215 -4
  19. package/dist_ts/integrations/esphome/esphome.classes.client.d.ts +19 -7
  20. package/dist_ts/integrations/esphome/esphome.classes.client.js +183 -89
  21. package/dist_ts/integrations/esphome/esphome.classes.configflow.d.ts +5 -2
  22. package/dist_ts/integrations/esphome/esphome.classes.configflow.js +43 -33
  23. package/dist_ts/integrations/esphome/esphome.classes.integration.d.ts +8 -6
  24. package/dist_ts/integrations/esphome/esphome.classes.integration.js +71 -43
  25. package/dist_ts/integrations/esphome/esphome.discovery.js +4 -12
  26. package/dist_ts/integrations/esphome/esphome.mapper.d.ts +2 -2
  27. package/dist_ts/integrations/esphome/esphome.mapper.js +6 -4
  28. package/dist_ts/integrations/esphome/esphome.types.d.ts +31 -6
  29. package/dist_ts/integrations/esphome/esphome.types.js +1 -1
  30. package/dist_ts/integrations/esphome/esphome.validation.d.ts +7 -0
  31. package/dist_ts/integrations/esphome/esphome.validation.js +66 -0
  32. package/dist_ts/integrations/homeassistant/homeassistant.classes.client.d.ts +32 -4
  33. package/dist_ts/integrations/homeassistant/homeassistant.classes.client.js +345 -116
  34. package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.d.ts +8 -5
  35. package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.js +34 -8
  36. package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.d.ts +7 -4
  37. package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.js +44 -56
  38. package/dist_ts/integrations/homeassistant/homeassistant.types.d.ts +12 -1
  39. package/dist_ts/integrations/hue/hue.classes.client.d.ts +6 -1
  40. package/dist_ts/integrations/hue/hue.classes.client.js +120 -45
  41. package/dist_ts/integrations/hue/hue.classes.configflow.js +16 -7
  42. package/dist_ts/integrations/hue/hue.classes.integration.d.ts +2 -0
  43. package/dist_ts/integrations/hue/hue.classes.integration.js +103 -30
  44. package/dist_ts/integrations/hue/hue.validation.d.ts +1 -0
  45. package/dist_ts/integrations/hue/hue.validation.js +14 -4
  46. package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
  47. package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
  48. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
  49. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
  50. package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
  51. package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
  52. package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
  53. package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
  54. package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
  55. package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
  56. package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
  57. package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
  58. package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
  59. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
  60. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
  61. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
  62. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
  63. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
  64. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
  65. package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
  66. package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
  67. package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
  68. package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
  69. package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
  70. package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
  71. package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
  72. package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
  73. package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
  74. package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
  75. package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
  76. package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
  77. package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
  78. package/dist_ts/integrations/shelly/shelly.types.js +11 -2
  79. package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
  80. package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
  81. package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
  82. package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
  83. package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
  84. package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
  85. package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
  86. package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
  87. package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
  88. package/dist_ts/integrations/sonos/sonos.config.js +23 -2
  89. package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
  90. package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
  91. package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
  92. package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
  93. package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
  94. package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
  95. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
  96. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
  97. package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
  98. package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
  99. package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
  100. package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
  101. package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
  102. package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
  103. package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
  104. package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
  105. package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
  106. package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
  107. package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
  108. package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
  109. package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
  110. package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
  111. package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
  112. package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
  113. package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
  114. package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
  115. package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
  116. package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
  117. package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
  118. package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
  119. package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
  120. package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
  121. package/dist_ts/integrations/webostv/webostv.types.js +1 -1
  122. package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
  123. package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
  124. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
  125. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
  126. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
  127. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
  128. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
  129. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
  130. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
  131. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
  132. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
  133. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
  134. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
  135. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
  136. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
  137. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
  138. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
  139. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
  140. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
  141. package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
  142. package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
  143. package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
  144. package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
  145. package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
  146. package/dist_ts/plugins.d.ts +2 -1
  147. package/dist_ts/plugins.js +3 -2
  148. package/package.json +8 -8
  149. package/readme.md +25 -6
  150. package/ts/00_commitinfo_data.ts +1 -1
  151. package/ts/core/classes.nativeintegrationowner.ts +5 -0
  152. package/ts/core/classes.runtimemanager.ts +11 -2
  153. package/ts/core/types.ts +2 -0
  154. package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
  155. package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
  156. package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
  157. package/ts/integrations/deconz/deconz.mapper.ts +22 -5
  158. package/ts/integrations/deconz/deconz.types.ts +49 -6
  159. package/ts/integrations/deconz/deconz.validation.ts +177 -4
  160. package/ts/integrations/deconz/readme.md +15 -1
  161. package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
  162. package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
  163. package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
  164. package/ts/integrations/esphome/esphome.discovery.ts +3 -12
  165. package/ts/integrations/esphome/esphome.mapper.ts +9 -7
  166. package/ts/integrations/esphome/esphome.types.ts +35 -6
  167. package/ts/integrations/esphome/esphome.validation.ts +66 -0
  168. package/ts/integrations/esphome/readme.md +20 -6
  169. package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
  170. package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
  171. package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
  172. package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
  173. package/ts/integrations/homeassistant/readme.md +22 -6
  174. package/ts/integrations/hue/hue.classes.client.ts +110 -41
  175. package/ts/integrations/hue/hue.classes.configflow.ts +12 -6
  176. package/ts/integrations/hue/hue.classes.integration.ts +92 -26
  177. package/ts/integrations/hue/hue.validation.ts +13 -3
  178. package/ts/integrations/hue/readme.md +3 -3
  179. package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
  180. package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
  181. package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
  182. package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
  183. package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
  184. package/ts/integrations/mqtt/mqtt.types.ts +52 -0
  185. package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
  186. package/ts/integrations/mqtt/readme.md +12 -3
  187. package/ts/integrations/samsungtv/readme.md +11 -5
  188. package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
  189. package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
  190. package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
  191. package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
  192. package/ts/integrations/shelly/readme.md +22 -7
  193. package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
  194. package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
  195. package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
  196. package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
  197. package/ts/integrations/shelly/shelly.mapper.ts +19 -20
  198. package/ts/integrations/shelly/shelly.rpc.ts +2 -0
  199. package/ts/integrations/shelly/shelly.types.ts +50 -2
  200. package/ts/integrations/shelly/shelly.validation.ts +76 -0
  201. package/ts/integrations/sonos/readme.md +117 -38
  202. package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
  203. package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
  204. package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
  205. package/ts/integrations/sonos/sonos.config.ts +24 -2
  206. package/ts/integrations/sonos/sonos.mapper.ts +13 -8
  207. package/ts/integrations/sonos/sonos.matchers.ts +2 -2
  208. package/ts/integrations/sonos/sonos.types.ts +30 -1
  209. package/ts/integrations/tasmota/readme.md +16 -5
  210. package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
  211. package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
  212. package/ts/integrations/tasmota/tasmota.config.ts +17 -0
  213. package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
  214. package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
  215. package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
  216. package/ts/integrations/tasmota/tasmota.types.ts +43 -2
  217. package/ts/integrations/tuya/readme.md +52 -10
  218. package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
  219. package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
  220. package/ts/integrations/tuya/tuya.mapper.ts +1 -1
  221. package/ts/integrations/tuya/tuya.types.ts +4 -0
  222. package/ts/integrations/tuya/tuya.validation.ts +5 -8
  223. package/ts/integrations/webostv/readme.md +115 -22
  224. package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
  225. package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
  226. package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
  227. package/ts/integrations/webostv/webostv.mapper.ts +4 -4
  228. package/ts/integrations/webostv/webostv.types.ts +18 -3
  229. package/ts/integrations/webostv/webostv.validation.ts +22 -1
  230. package/ts/integrations/zigbee2mqtt/readme.md +22 -27
  231. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
  232. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
  233. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
  234. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
  235. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
  236. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
  237. package/ts/integrations/zwave_js/readme.md +42 -9
  238. package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
  239. package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
  240. package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
  241. package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
  242. package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
  243. package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
  244. package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
  245. package/ts/plugins.ts +2 -1
@@ -0,0 +1,76 @@
1
+ import type { IShellyConfig, IShellyIntegrationOptions } from './shelly.types.js';
2
+
3
+ export const shellyData = (valueArg: unknown, allowedArg?: readonly string[]): Record<string, unknown> => {
4
+ if (!valueArg || typeof valueArg !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(valueArg))) {
5
+ throw new TypeError('Shelly input must be a plain data object.');
6
+ }
7
+ const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
8
+ for (const key of Reflect.ownKeys(valueArg)) {
9
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
10
+ if (typeof key !== 'string' || allowedArg && !allowedArg.includes(key) || !descriptor || !descriptor.enumerable || !('value' in descriptor)) {
11
+ throw new TypeError('Shelly input contains an unsupported field or accessor.');
12
+ }
13
+ result[key] = descriptor.value as unknown;
14
+ }
15
+ return result;
16
+ };
17
+
18
+ export const shellyText = (valueArg: unknown, labelArg: string, maximumArg = 128): string => {
19
+ if (typeof valueArg !== 'string' || !valueArg || valueArg.trim() !== valueArg || valueArg.length > maximumArg || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
20
+ throw new TypeError(`Shelly ${labelArg} must be bounded text without control characters.`);
21
+ }
22
+ return valueArg;
23
+ };
24
+
25
+ const integer = (valueArg: unknown, minimumArg: number, maximumArg: number): number => {
26
+ if (typeof valueArg !== 'number' || !Number.isSafeInteger(valueArg) || valueArg < minimumArg || valueArg > maximumArg) {
27
+ throw new TypeError('Shelly integer is outside its supported bounds.');
28
+ }
29
+ return valueArg;
30
+ };
31
+
32
+ export const shellyConfigFields = ['homeId', 'host', 'port', 'protocol', 'expectedDeviceId', 'expectedMacAddress',
33
+ 'expectedModel', 'expectedGeneration', 'credentialRef', 'allowControl', 'allowUnencryptedLan',
34
+ 'requestTimeoutMs', 'maxPendingOperations', 'maxResponseBytes'] as const;
35
+
36
+ export type TNormalizedShellyConfig = Readonly<Required<Omit<IShellyConfig, 'credentialRef'>> & Pick<IShellyConfig, 'credentialRef'>>;
37
+
38
+ export const normalizeShellyConfig = (valueArg: unknown): TNormalizedShellyConfig => {
39
+ const config = shellyData(valueArg, shellyConfigFields);
40
+ const host = shellyText(config.host, 'host', 253);
41
+ if (/[\s\/?#@]/.test(host)) throw new TypeError('Shelly host must be a hostname or IP address.');
42
+ const protocol = config.protocol ?? 'http';
43
+ if (protocol !== 'http' && protocol !== 'https') throw new TypeError('Shelly protocol must be http or https.');
44
+ const expectedDeviceId = shellyText(config.expectedDeviceId, 'selected device id');
45
+ if (!/^shelly[\w-]+$/i.test(expectedDeviceId)) throw new TypeError('Shelly requires an exact reported device id.');
46
+ const mac = shellyText(config.expectedMacAddress, 'selected MAC', 12);
47
+ if (!/^[0-9a-f]{12}$/i.test(mac) || /^(?:0{12}|f{12})$/i.test(mac) || (Number.parseInt(mac.slice(0, 2), 16) & 1) !== 0) {
48
+ throw new TypeError('Shelly requires an exact usable reported 12-digit MAC.');
49
+ }
50
+ if (config.expectedGeneration !== 2 && config.expectedGeneration !== 3) throw new TypeError('Shelly selected generation must be 2 or 3.');
51
+ if (config.allowControl !== undefined && typeof config.allowControl !== 'boolean' || config.allowUnencryptedLan !== undefined && typeof config.allowUnencryptedLan !== 'boolean') {
52
+ throw new TypeError('Shelly consent settings must be boolean.');
53
+ }
54
+ if (protocol === 'http' && config.allowUnencryptedLan !== true) throw new TypeError('Shelly HTTP requires explicit unencrypted LAN consent.');
55
+ const port = integer(config.port ?? (protocol === 'https' ? 443 : 80), 1, 65535);
56
+ const urlHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
57
+ try { new URL(`${protocol}://${urlHost}:${port}`); } catch { throw new TypeError('Shelly host must be a hostname or IP address.'); }
58
+ return Object.freeze({ homeId: shellyText(config.homeId, 'home ID', 256), host, port, protocol,
59
+ expectedDeviceId, expectedMacAddress: mac.toUpperCase(), expectedModel: shellyText(config.expectedModel, 'selected model'),
60
+ expectedGeneration: config.expectedGeneration,
61
+ ...(config.credentialRef === undefined ? {} : { credentialRef: shellyText(config.credentialRef, 'credential reference', 256) }),
62
+ allowControl: config.allowControl === true, allowUnencryptedLan: config.allowUnencryptedLan === true,
63
+ requestTimeoutMs: integer(config.requestTimeoutMs ?? 10000, 10, 60000),
64
+ maxPendingOperations: integer(config.maxPendingOperations ?? 16, 1, 128),
65
+ maxResponseBytes: integer(config.maxResponseBytes ?? 1048576, 1024, 1048576) });
66
+ };
67
+
68
+ export const normalizeShellyOptions = (valueArg: unknown = {}): Readonly<IShellyIntegrationOptions> => {
69
+ const options = shellyData(valueArg, ['homeId', 'capacity', 'resolvePassword']);
70
+ if (options.resolvePassword !== undefined && typeof options.resolvePassword !== 'function') throw new TypeError('Shelly password resolver must be a function.');
71
+ return Object.freeze({
72
+ ...(options.homeId === undefined ? {} : { homeId: shellyText(options.homeId, 'owning home ID', 256) }),
73
+ ...(options.capacity === undefined ? {} : { capacity: integer(options.capacity, 1, Number.MAX_SAFE_INTEGER) }),
74
+ ...(options.resolvePassword === undefined ? {} : { resolvePassword: options.resolvePassword as IShellyIntegrationOptions['resolvePassword'] }),
75
+ });
76
+ };
@@ -1,16 +1,19 @@
1
1
  # Sonos
2
2
 
3
3
  The native local runtime uses the public UPnP client and transport/volume controllers
4
- from `@ecobridge.xyz/devicemanager`. It reads the configured renderer's device
5
- description, transport, position, volume, mute and ZoneGroupTopology over bounded
6
- HTTP/SOAP. No Home Assistant instance or Sonos cloud account is required.
4
+ from `@ecobridge.xyz/devicemanager`. It reads the selected renderer's device
5
+ identity, transport, position, volume, mute, reported topology and service
6
+ capabilities through bounded HTTP/SOAP. No Home Assistant instance, Sonos cloud
7
+ account or invented authentication provider is required for this selected protocol.
7
8
 
8
9
  ```typescript
9
- const runtime = await new SonosIntegration().setup({
10
+ const integration = new SonosIntegration({ allowControl: true });
11
+ const runtime = await integration.setup({
12
+ homeId: 'home:example',
10
13
  host: '192.168.1.55',
11
14
  expectedUid: 'RINCON_000E58ABCDEF01400',
12
15
  pollIntervalMs: 5000,
13
- }, { abortSignal });
16
+ }, { homeId: 'home:example', abortSignal });
14
17
 
15
18
  const [device] = await runtime.devices();
16
19
  await runtime.writeFeature!({
@@ -18,57 +21,133 @@ await runtime.writeFeature!({
18
21
  feature: device.features.find((feature) => feature.id === 'volume')!,
19
22
  value: 35,
20
23
  });
21
- await runtime.destroy();
24
+ await integration.destroy();
22
25
  ```
23
26
 
27
+ The embedding host must independently select `context.homeId`; configuration,
28
+ discovery, form values and storage scope cannot select it or grant control. The
29
+ owning home binds once, including after failed setup. An optional constructor
30
+ `homeId` only constrains that explicit context. The exported `SonosClient` also
31
+ requires configuration and context homes before allocating protocol resources:
32
+ `new SonosClient(config, { homeId, abortSignal }, { allowControl: true })`.
33
+ A matching actual `NoSqlConfigStore` is optional and borrowed; the adapter never
34
+ closes the caller's store, database or key provider. This credential-free adapter
35
+ has no persistence requirement and does not write application state to disk.
36
+
37
+ Controls default to denied. Only the embedding host's immutable constructor
38
+ `allowControl: true` enables them; saved configuration and config-flow submissions
39
+ cannot set this option. A home identifier is a host scope, not an authentication
40
+ credential. UPnP assumes a trusted local network. Sonos currently documents its
41
+ UPnP interface as unsupported and provides an owner-controlled connection-security
42
+ setting; the adapter does not change that setting or bypass it.
43
+
24
44
  Configuration requires a bare IPv4 address or DNS hostname. The default port is
25
- 1400. Optional identity pinning rejects a different renderer. Request deadlines
45
+ 1400. Optional UUID pinning rejects a different renderer. Whole-operation deadlines
26
46
  default to 10 seconds and response bodies to 1 MiB. Production snapshot fixtures,
27
- credentials in URLs and arbitrary configuration fields are rejected.
47
+ credentials in URLs and arbitrary configuration fields are rejected. Config flow
48
+ captures the selected home and endpoint/identity hint; it exposes host and port,
49
+ rejects a conflicting submitted home, and returns the captured home without
50
+ performing discovery or granting authority.
28
51
 
29
- The discovery descriptor owns an active native SSDP probe. Its observation window
30
- defaults to three seconds, with a separate five-second startup budget. Caller
31
- cancellation reaches the owning SSDP lifecycle and drains sockets and description
32
- requests. `IDiscoveryContext.networkInterfaces` limits discovery to the selected
33
- actual interfaces; an explicitly empty scope performs no network I/O. Passive
34
- SSDP and explicit Sonos mDNS/manual candidates use the same strict admission rules.
35
- Discovered endpoints are verified during setup before becoming available devices.
52
+ Device IDs are `sonos.player.<encoded-home>.<renderer-uuid>` and entity IDs are
53
+ `media_player.sonos.<encoded-home>.<renderer-uuid>`. Home segments use
54
+ `encodeURIComponent`; UUIDs are lowercase. These home-prefixed IDs intentionally
55
+ replace the previous unscoped IDs. Room renames preserve identity. Group metadata
56
+ comes from fresh reported topology; advertised member URLs are never followed.
57
+ Playback requires the selected renderer to remain its group coordinator. A grouped
58
+ member exposes its actual state and supports its own advertised rendering controls
59
+ without redirecting playback. Bonded satellites require selecting their parent.
36
60
 
37
- Devices and entities use renderer UUIDs, so room renames preserve identity. Group
38
- metadata comes from the renderer's reported topology; advertised member URLs are
39
- never followed. Playback controls require the selected renderer to be its group
40
- coordinator. A grouped member exposes its actual state and supports its own volume
41
- and mute controls without silently redirecting playback to another renderer.
42
- Bonded satellites require selecting their parent renderer.
61
+ Before each transport effect, the adapter refreshes renderer identity, topology,
62
+ the selected service description and `GetCurrentTransportActions`. Play, Pause,
63
+ Stop, Next and Previous require both a declared action and its current action token.
64
+ `SetAVTransportURI` is declared by its service description; the standard current
65
+ transport list describes actions on the current resource and does not include URI
66
+ assignment. Volume and mute use separate, fresh RenderingControl declarations.
67
+ Volume also checks `GetOutputFixed` when that action is advertised; devices without
68
+ that action are judged by their actual `SetVolume` declaration, without fabricating
69
+ a fixed-output observation. AVTransport actions never grant volume permission.
70
+ Writable flags describe the observed capability and owner permission; they do not
71
+ reserve authority for a later write.
43
72
 
44
73
  Feature writes support integer volume from 0 to 100, boolean mute and playback
45
74
  values `playing`, `paused` or `stopped`. Media-player services support `play`,
46
75
  `pause`, `stop`, `next_track`, `previous_track`, `volume_set`, `volume_mute` and
47
76
  `play_media`. Each service requires an exact selected device or entity target and
48
77
  its documented arguments. `volume_level` is a fraction from 0 to 1; `volume` is an
49
- integer percent. A successful SOAP acknowledgement is followed by a fresh read;
50
- state is never optimistically changed and failed or uncertain calls are not retried.
78
+ integer percent.
79
+
80
+ Each supported action is sent once and is never automatically replayed. A SOAP
81
+ acknowledgement is followed by fresh independent observations. Returned
82
+ `acknowledgedActions`, `outcome`, `requestedValue` and `observedValue` distinguish
83
+ an observed target, a different observed value, or an observation without a
84
+ comparable target. `success: true` means acknowledged actions and successful fresh
85
+ readback; it does not assert physical or acoustic causation. Results explicitly
86
+ carry `physicalEffectConfirmed: false`; runtime service receipts additionally carry
87
+ `observationAtomic: false`. A failed
88
+ command rejects with `SonosCommandError`, retaining acknowledged actions and a
89
+ conservative `not-sent`, `rejected` or `indeterminate` effect outcome. An acknowledged
90
+ write followed by read failure is indeterminate, without cached success or retry.
91
+
92
+ `play_media` preserves the documented sequential `SetAVTransportURI` then `Play`
93
+ behavior within one admitted operation and one whole-operation deadline. Identity,
94
+ coordinator and relevant action authority are refreshed between those distinct
95
+ steps. If the second step fails, the URI assignment can remain acknowledged and
96
+ is not rolled back or replayed. Separate preflight, action and readback exchanges
97
+ can race another controller or a topology change; UPnP offers no atomic transaction
98
+ across them. Successful readback cannot prove that this action caused the observed
99
+ state or that audio was heard.
100
+
51
101
  `play_media` accepts `uri` or `media_content_id`. Supplying `title` also requires
52
- an explicit `mime_type`, such as `audio/mpeg` or `audio/flac`, for its audio DIDL
53
- metadata. The runtime does not guess the media format from an arbitrary URI.
54
- Stream URLs require an explicit hostname and `//` authority, using `http:`,
55
- `https:`, `x-sonos-http:` or `x-rincon-mp3radio:` without embedded credentials.
56
- Direct client calls use `playUri(uri, { title, mimeType }, abortSignal)`.
102
+ an explicit `mime_type`, such as `audio/mpeg` or `audio/flac`, for audio DIDL
103
+ metadata. The runtime does not guess media format. Stream URLs require an explicit
104
+ hostname and `//` authority, using `http:`, `https:`, `x-sonos-http:` or
105
+ `x-rincon-mp3radio:` without embedded credentials. Direct clients use
106
+ `playUri(uri, { title, mimeType }, abortSignal)` and return the acknowledged actions,
107
+ outcome and fresh snapshot as `ISonosControlResult`.
108
+
109
+ Setup, live runtimes, runtime/client operations, config-flow continuations and
110
+ retained failed cleanup share one owner with capacity 16. A live runtime and one
111
+ operation occupy two slots. Admission occurs before protocol allocation or caller
112
+ callbacks. The integration closes its flow and all owned runtimes; runtime destroy
113
+ closes that runtime's lease. Close promises are stable before abort callbacks,
114
+ terminal admission is enforced, and cleanup settles every sibling plus actual
115
+ pending commands and subscriber promises. Failed cleanup retains the actual handle
116
+ until explicit `integration.retryCleanup()` or `client.retryCleanup()` succeeds;
117
+ this retries cleanup only, never an effect. A failed original close promise remains
118
+ failed after retry, preserving its reported outcome.
57
119
 
58
120
  Subscriptions are explicitly `local_poll`, with a default five-second interval.
59
- Each subscription owns one sequential poll, cancellation and cleanup. Failures
60
- emit unavailable/error events rather than manufacturing known state; subsequent
61
- scheduled reads can report recovery. Unsubscribe and destroy abort and drain
62
- pending work. There is no GENA push capability, grouping mutation, queue management,
63
- favorites, alarms or EQ in this runtime.
121
+ Each subscription reserves a slot before its initial observation/callback, owns
122
+ one sequential poll, cancellation and actual callback drain, and is limited to
123
+ 16 per runtime. Async subscriber promises are awaited even though the generic
124
+ callback signature returns void. Unsubscribe and destroy abort protocol work and
125
+ wait for actual callback completion; a callback that ignores cancellation must
126
+ settle before cleanup completes. Read failures emit unavailable/error events;
127
+ subsequent scheduled reads can report recovery. Subscriber failures remain
128
+ observable through cleanup. There is no GENA push capability, grouping mutation,
129
+ queue management, favorites, alarms or EQ in this runtime.
130
+
131
+ The discovery descriptor owns an active native SSDP probe. Its observation window
132
+ defaults to three seconds, with a separate five-second startup budget. Caller
133
+ cancellation drains sockets and description requests.
134
+ `IDiscoveryContext.networkInterfaces` limits discovery to selected actual
135
+ interfaces; an explicitly empty scope performs no network I/O. Passive SSDP and
136
+ explicit Sonos mDNS/manual candidates use the same strict admission rules.
137
+ Discovered endpoints are verified during setup before becoming available devices.
64
138
 
65
139
  Protocol references:
66
140
 
67
141
  - [devicemanager UPnP contracts](https://code.foss.global/ecobridge.xyz/devicemanager)
142
+ - [UPnP AVTransport:1 specification, especially sections 2.2.26 and 2.4.17](https://upnp.org/specs/av/UPnP-av-AVTransport-v1-Service.pdf)
143
+ - [UPnP RenderingControl:1 specification](https://upnp.org/specs/av/UPnP-av-RenderingControl-v1-Service.pdf)
144
+ - [Sonos connection security settings](https://support.sonos.com/en/article/adjust-connection-security-settings)
68
145
  - [SoCo's native Sonos service implementation](https://github.com/SoCo/SoCo/blob/master/soco/services.py)
69
- - [SoCo's ZoneGroupState XML contract](https://github.com/SoCo/SoCo/blob/master/soco/zonegroupstate.py)
146
+ - [SoCo's ZoneGroupState contract](https://github.com/SoCo/SoCo/blob/master/soco/zonegroupstate.py)
147
+ - [SoCo's current-action and fixed-output implementation](https://github.com/SoCo/SoCo/blob/master/soco/core.py)
70
148
 
71
- The local UPnP wire contract was checked against those primary implementation
72
- sources. Sonos's current public developer portal documents its cloud Control API;
73
- it does not provide an openly accessible complete local UPnP specification.
74
- Verification uses local real HTTP/SOAP fixtures; owner hardware was not accessed.
149
+ The selected wire contract was checked against the standards, maintained primary
150
+ implementation and actual installed public devicemanager 4.8.0 source/declarations.
151
+ Sonos's public developer portal documents its Control API; a complete current
152
+ Sonos-authored local UPnP specification was not found. Verification uses real local
153
+ HTTP/SOAP and SSDP fixtures; owner hardware was not accessed.