@smarthome.exchange/integrations 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/changelog.md +46 -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 +32 -4
  40. package/dist_ts/integrations/hue/hue.classes.client.js +331 -57
  41. package/dist_ts/integrations/hue/hue.classes.configflow.d.ts +6 -1
  42. package/dist_ts/integrations/hue/hue.classes.configflow.js +79 -27
  43. package/dist_ts/integrations/hue/hue.classes.integration.d.ts +3 -1
  44. package/dist_ts/integrations/hue/hue.classes.integration.js +83 -62
  45. package/dist_ts/integrations/hue/hue.mapper.d.ts +2 -0
  46. package/dist_ts/integrations/hue/hue.mapper.js +8 -6
  47. package/dist_ts/integrations/hue/hue.types.d.ts +32 -1
  48. package/dist_ts/integrations/hue/hue.validation.d.ts +9 -3
  49. package/dist_ts/integrations/hue/hue.validation.js +112 -11
  50. package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
  51. package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
  52. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
  53. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
  54. package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
  55. package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
  56. package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
  57. package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
  58. package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
  59. package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
  60. package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
  61. package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
  62. package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
  63. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
  64. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
  65. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
  66. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
  67. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
  68. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
  69. package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
  70. package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
  71. package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
  72. package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
  73. package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
  74. package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
  75. package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
  76. package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
  77. package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
  78. package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
  79. package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
  80. package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
  81. package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
  82. package/dist_ts/integrations/shelly/shelly.types.js +11 -2
  83. package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
  84. package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
  85. package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
  86. package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
  87. package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
  88. package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
  89. package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
  90. package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
  91. package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
  92. package/dist_ts/integrations/sonos/sonos.config.js +23 -2
  93. package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
  94. package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
  95. package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
  96. package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
  97. package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
  98. package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
  99. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
  100. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
  101. package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
  102. package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
  103. package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
  104. package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
  105. package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
  106. package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
  107. package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
  108. package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
  109. package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
  110. package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
  111. package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
  112. package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
  113. package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
  114. package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
  115. package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
  116. package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
  117. package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
  118. package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
  119. package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
  120. package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
  121. package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
  122. package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
  123. package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
  124. package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
  125. package/dist_ts/integrations/webostv/webostv.types.js +1 -1
  126. package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
  127. package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
  128. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
  129. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
  130. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
  131. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
  132. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
  133. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
  134. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
  135. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
  136. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
  137. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
  138. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
  139. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
  140. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
  141. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
  142. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
  143. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
  144. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
  145. package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
  146. package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
  147. package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
  148. package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
  149. package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
  150. package/dist_ts/plugins.d.ts +2 -1
  151. package/dist_ts/plugins.js +3 -2
  152. package/package.json +10 -10
  153. package/readme.md +25 -6
  154. package/ts/00_commitinfo_data.ts +1 -1
  155. package/ts/core/classes.nativeintegrationowner.ts +5 -0
  156. package/ts/core/classes.runtimemanager.ts +11 -2
  157. package/ts/core/types.ts +2 -0
  158. package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
  159. package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
  160. package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
  161. package/ts/integrations/deconz/deconz.mapper.ts +22 -5
  162. package/ts/integrations/deconz/deconz.types.ts +49 -6
  163. package/ts/integrations/deconz/deconz.validation.ts +177 -4
  164. package/ts/integrations/deconz/readme.md +15 -1
  165. package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
  166. package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
  167. package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
  168. package/ts/integrations/esphome/esphome.discovery.ts +3 -12
  169. package/ts/integrations/esphome/esphome.mapper.ts +9 -7
  170. package/ts/integrations/esphome/esphome.types.ts +35 -6
  171. package/ts/integrations/esphome/esphome.validation.ts +66 -0
  172. package/ts/integrations/esphome/readme.md +20 -6
  173. package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
  174. package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
  175. package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
  176. package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
  177. package/ts/integrations/homeassistant/readme.md +22 -6
  178. package/ts/integrations/hue/hue.classes.client.ts +288 -55
  179. package/ts/integrations/hue/hue.classes.configflow.ts +68 -25
  180. package/ts/integrations/hue/hue.classes.integration.ts +75 -54
  181. package/ts/integrations/hue/hue.mapper.ts +8 -5
  182. package/ts/integrations/hue/hue.types.ts +37 -1
  183. package/ts/integrations/hue/hue.validation.ts +102 -13
  184. package/ts/integrations/hue/readme.md +49 -12
  185. package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
  186. package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
  187. package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
  188. package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
  189. package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
  190. package/ts/integrations/mqtt/mqtt.types.ts +52 -0
  191. package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
  192. package/ts/integrations/mqtt/readme.md +12 -3
  193. package/ts/integrations/samsungtv/readme.md +11 -5
  194. package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
  195. package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
  196. package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
  197. package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
  198. package/ts/integrations/shelly/readme.md +22 -7
  199. package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
  200. package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
  201. package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
  202. package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
  203. package/ts/integrations/shelly/shelly.mapper.ts +19 -20
  204. package/ts/integrations/shelly/shelly.rpc.ts +2 -0
  205. package/ts/integrations/shelly/shelly.types.ts +50 -2
  206. package/ts/integrations/shelly/shelly.validation.ts +76 -0
  207. package/ts/integrations/sonos/readme.md +117 -38
  208. package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
  209. package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
  210. package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
  211. package/ts/integrations/sonos/sonos.config.ts +24 -2
  212. package/ts/integrations/sonos/sonos.mapper.ts +13 -8
  213. package/ts/integrations/sonos/sonos.matchers.ts +2 -2
  214. package/ts/integrations/sonos/sonos.types.ts +30 -1
  215. package/ts/integrations/tasmota/readme.md +16 -5
  216. package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
  217. package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
  218. package/ts/integrations/tasmota/tasmota.config.ts +17 -0
  219. package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
  220. package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
  221. package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
  222. package/ts/integrations/tasmota/tasmota.types.ts +43 -2
  223. package/ts/integrations/tuya/readme.md +52 -10
  224. package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
  225. package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
  226. package/ts/integrations/tuya/tuya.mapper.ts +1 -1
  227. package/ts/integrations/tuya/tuya.types.ts +4 -0
  228. package/ts/integrations/tuya/tuya.validation.ts +5 -8
  229. package/ts/integrations/webostv/readme.md +115 -22
  230. package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
  231. package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
  232. package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
  233. package/ts/integrations/webostv/webostv.mapper.ts +4 -4
  234. package/ts/integrations/webostv/webostv.types.ts +18 -3
  235. package/ts/integrations/webostv/webostv.validation.ts +22 -1
  236. package/ts/integrations/zigbee2mqtt/readme.md +22 -27
  237. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
  238. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
  239. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
  240. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
  241. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
  242. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
  243. package/ts/integrations/zwave_js/readme.md +42 -9
  244. package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
  245. package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
  246. package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
  247. package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
  248. package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
  249. package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
  250. package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
  251. package/ts/plugins.ts +2 -1
@@ -1,47 +1,57 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, ILogger, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
2
+ import type { INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
3
+ import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
3
4
  import type { IHomeAssistantTuyaConfig, TTuyaDatapointMapping } from './tuya.types.js';
4
5
  import { tuyaDeviceId, tuyaDevices, tuyaEntities, tuyaEntityId, tuyaFeatureId, type ITuyaObservedDatapoint } from './tuya.mapper.js';
5
6
  import { tuyaKeys, tuyaRecord, tuyaValueMatches } from './tuya.validation.js';
6
7
 
8
+ interface ITuyaCleanupResource {
9
+ readonly cleanup: () => Promise<void>;
10
+ promise?: Promise<void>;
11
+ failed: boolean;
12
+ }
13
+
7
14
  export class TuyaRuntime implements IIntegrationRuntime {
8
15
  public readonly domain = 'tuya';
9
16
  readonly #config: IHomeAssistantTuyaConfig;
10
17
  readonly #protocol: plugins.ecobridgeDeviceManager.TuyaProtocol;
11
- readonly #onDestroyed: () => void;
12
- readonly #logger?: ILogger;
18
+ readonly #lease: INativeSetupLease;
13
19
  readonly #observed = new Map<number, ITuyaObservedDatapoint>();
14
20
  readonly #subscriptions = new Set<(eventArg: IIntegrationEvent) => void>();
15
21
  readonly #subscriptionCleanup = new Set<() => void>();
16
- readonly #unsubscribe: () => void;
17
- readonly #unsubscribeErrors: () => void;
22
+ readonly #resources: ITuyaCleanupResource[] = [];
23
+ readonly #callbacks = new Set<Promise<void>>();
24
+ #subscriberFailure = false;
18
25
  #closed = false;
19
26
  #destroy?: Promise<void>;
20
27
 
21
28
  constructor(configArg: IHomeAssistantTuyaConfig, protocolArg: plugins.ecobridgeDeviceManager.TuyaProtocol,
22
- reportsArg: readonly plugins.ecobridgeDeviceManager.ITuyaDatapointReport[], onDestroyedArg: () => void, loggerArg?: ILogger) {
29
+ reportsArg: readonly plugins.ecobridgeDeviceManager.ITuyaDatapointReport[], leaseArg: INativeSetupLease) {
23
30
  this.#config = configArg;
24
31
  this.#protocol = protocolArg;
25
- this.#onDestroyed = onDestroyedArg;
26
- this.#logger = loggerArg;
32
+ this.#lease = leaseArg;
33
+ this.#resources.push({ cleanup: () => this.#protocol.close(), failed: false });
27
34
  for (const report of reportsArg) this.#accept(report);
28
35
  if (this.#observed.size === 0) throw new Error('Tuya device reported none of the configured product datapoints.');
29
- this.#unsubscribe = this.#protocol.subscribe((report) => {
36
+ const unsubscribe = this.#protocol.subscribe((report) => {
37
+ if (this.#lease.signal.aborted || this.#destroy) return;
30
38
  try { this.#accept(report); }
31
39
  catch {
32
40
  this.#closed = true;
33
41
  this.#emit({ type: 'error', data: { code: 'schema-mismatch', message: 'Tuya reported data no longer matches the configured product schema.' } });
34
42
  this.#emit({ type: 'availability_changed', data: { available: false } });
35
- void this.destroy();
43
+ void this.destroy().catch(() => undefined);
36
44
  }
37
45
  });
38
- this.#unsubscribeErrors = this.#protocol.subscribeErrors((error) => {
46
+ this.#resources.push({ cleanup: async () => unsubscribe(), failed: false });
47
+ const unsubscribeErrors = this.#protocol.subscribeErrors((error) => {
39
48
  this.#emit({ type: 'error', data: { code: error.code, effectOutcome: error.effectOutcome } });
40
49
  if (!this.#protocol.connected) this.#emit({ type: 'availability_changed', data: { available: false } });
41
50
  });
51
+ this.#resources.push({ cleanup: async () => unsubscribeErrors(), failed: false });
42
52
  }
43
53
 
44
- public isHealthy(): boolean { return !this.#closed && this.#protocol.connected; }
54
+ public isHealthy(): boolean { return !this.#closed && !this.#lease.signal.aborted && this.#protocol.connected; }
45
55
 
46
56
  public async devices(signalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
47
57
  signalArg?.throwIfAborted();
@@ -53,6 +63,7 @@ export class TuyaRuntime implements IIntegrationRuntime {
53
63
  }
54
64
 
55
65
  public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, signalArg?: AbortSignal): Promise<() => Promise<void>> {
66
+ this.#lease.assertOpen();
56
67
  if (this.#closed) throw new Error('Tuya runtime is permanently closed.');
57
68
  if (typeof handlerArg !== 'function') throw new TypeError('Tuya subscription handler must be a function.');
58
69
  signalArg?.throwIfAborted();
@@ -106,16 +117,37 @@ export class TuyaRuntime implements IIntegrationRuntime {
106
117
  } catch (errorArg) { return this.#failure(errorArg); }
107
118
  }
108
119
 
109
- public destroy(): Promise<void> {
110
- if (this.#destroy) return this.#destroy;
111
- this.#closed = true;
112
- this.#unsubscribe();
113
- this.#unsubscribeErrors();
114
- for (const cleanup of [...this.#subscriptionCleanup]) cleanup();
115
- this.#destroy = this.#protocol.close().then(() => this.#onDestroyed());
120
+ public destroy(): Promise<void> { return this.#lease.close(); }
121
+
122
+ /** Invoked by the retained owner handle, including explicit cleanup-only retry. */
123
+ public closeOwnedResources(retryArg = false): Promise<void> {
124
+ if (retryArg) return this.#drain(true);
125
+ if (!this.#destroy) {
126
+ this.#closed = true;
127
+ this.#destroy = Promise.resolve().then(() => this.#drain(false));
128
+ }
116
129
  return this.#destroy;
117
130
  }
118
131
 
132
+ async #drain(retryArg: boolean): Promise<void> {
133
+ for (const cleanup of [...this.#subscriptionCleanup]) cleanup();
134
+ // The first close already joined every admitted callback. Explicit retry acknowledges its
135
+ // retained failure; subscriber code is never invoked again as part of cleanup.
136
+ if (retryArg) this.#subscriberFailure = false;
137
+ const cleanup = this.#resources.map((resourceArg) => {
138
+ if (retryArg && resourceArg.failed) { resourceArg.promise = undefined; resourceArg.failed = false; }
139
+ resourceArg.promise ??= Promise.resolve().then(resourceArg.cleanup).catch(() => {
140
+ resourceArg.failed = true;
141
+ throw new Error('Tuya owned resource cleanup failed.');
142
+ });
143
+ return resourceArg.promise;
144
+ });
145
+ const [results] = await Promise.all([Promise.allSettled(cleanup), Promise.allSettled([...this.#callbacks])]);
146
+ const failures = results.filter((resultArg): resultArg is PromiseRejectedResult => resultArg.status === 'rejected').map((resultArg) => resultArg.reason as unknown);
147
+ if (this.#subscriberFailure) failures.push(new Error('Tuya event subscriber failed.'));
148
+ if (failures.length) throw new AggregateError(failures, 'Tuya cleanup failed; ownership remains retained for explicit retry.');
149
+ }
150
+
119
151
  #accept(reportArg: plugins.ecobridgeDeviceManager.ITuyaDatapointReport): void {
120
152
  const changes: ITuyaObservedDatapoint[] = [];
121
153
  for (const mapping of this.#config.datapoints) {
@@ -134,6 +166,7 @@ export class TuyaRuntime implements IIntegrationRuntime {
134
166
  }
135
167
 
136
168
  async #write(mappingArg: TTuyaDatapointMapping, valueArg: unknown, signalArg?: AbortSignal): Promise<IServiceCallResult> {
169
+ this.#lease.assertOpen();
137
170
  if (!this.isHealthy()) throw new Error('Tuya session is unavailable; no automatic reconnect or retry is performed.');
138
171
  if (mappingArg.kind === 'sensor' || mappingArg.access !== 'read-write') throw new Error('Tuya datapoint is read-only.');
139
172
  if (!tuyaValueMatches(mappingArg, valueArg)) throw new TypeError('Tuya write value does not match its configured type/range/step.');
@@ -151,16 +184,29 @@ export class TuyaRuntime implements IIntegrationRuntime {
151
184
 
152
185
  #emit(eventArg: Omit<IIntegrationEvent, 'integrationDomain' | 'deviceId' | 'timestamp'>): void {
153
186
  for (const handler of [...this.#subscriptions]) {
154
- if (this.#destroy) return;
187
+ if (this.#destroy || this.#lease.signal.aborted) return;
188
+ if (this.#callbacks.size >= 128) {
189
+ this.#subscriberFailed();
190
+ void this.destroy().catch(() => undefined);
191
+ return;
192
+ }
193
+ // Reserve a continuation before entering foreign code, including synchronous close reentry.
194
+ let finish!: () => void;
195
+ const continuation = new Promise<void>((resolveArg) => { finish = resolveArg; });
196
+ this.#callbacks.add(continuation);
197
+ const settled = (): void => { this.#callbacks.delete(continuation); finish(); };
155
198
  try {
156
199
  const result: unknown = handler({ ...structuredClone(eventArg), integrationDomain: 'tuya', deviceId: tuyaDeviceId(this.#config), timestamp: Date.now() });
157
- if (result instanceof Promise) void result.catch(() => this.#subscriberFailed());
158
- } catch { this.#subscriberFailed(); }
200
+ if (result !== null && (typeof result === 'object' || typeof result === 'function')) {
201
+ void Promise.resolve(result).then(settled, () => { this.#subscriberFailed(); settled(); });
202
+ } else settled();
203
+ } catch { this.#subscriberFailed(); settled(); }
159
204
  }
160
205
  }
161
206
 
162
207
  #subscriberFailed(): void {
163
- if (this.#destroy) return;
164
- this.#logger?.log('error', 'Tuya event subscriber failed.', { code: 'subscriber-error' });
208
+ this.#subscriberFailure = true;
209
+ try { this.#lease.context.logger?.log('error', 'Tuya event subscriber failed.', { code: 'subscriber-error' }); }
210
+ catch { /* Logging cannot detach or replace a retained subscriber failure. */ }
165
211
  }
166
212
  }
@@ -25,7 +25,7 @@ export const tuyaDevices = (configArg: IHomeAssistantTuyaConfig, observedArg: re
25
25
  protocol: 'unknown', manufacturer: 'Tuya', online: availableArg,
26
26
  features: observedArg.map(({ mapping }) => ({
27
27
  id: tuyaFeatureId(mapping), name: mapping.name, readable: true,
28
- writable: mapping.kind !== 'sensor' && mapping.access === 'read-write',
28
+ writable: availableArg && mapping.kind !== 'sensor' && mapping.access === 'read-write',
29
29
  capability: mapping.kind === 'switch' ? 'switch' : mapping.kind === 'brightness' ? 'light' : 'sensor',
30
30
  ...(mapping.kind === 'sensor' && mapping.unit !== undefined ? { unit: mapping.unit } : {}),
31
31
  })),
@@ -1,3 +1,7 @@
1
+ import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
2
+
3
+ export interface ITuyaIntegrationOptions extends INativeIntegrationOwnerOptions {}
4
+
1
5
  export type TTuyaDatapointAccess = 'read' | 'read-write';
2
6
 
3
7
  export interface ITuyaDatapointIdentity {
@@ -97,15 +97,12 @@ export const normalizeTuyaConfig = (valueArg: unknown): IHomeAssistantTuyaConfig
97
97
  });
98
98
  };
99
99
 
100
- export const scopedTuyaStore = (configArg: IHomeAssistantTuyaConfig, contextArg: IIntegrationSetupContext): NoSqlConfigStore => {
100
+ /** Capture data fields before admission; the shared owner verifies home and actual store scope. */
101
+ export const tuyaSetupContext = (contextArg: IIntegrationSetupContext): IIntegrationSetupContext => {
101
102
  const context = tuyaRecord(contextArg, 'setup context');
102
- tuyaKeys(context, ['configStore', 'abortSignal', 'logger']);
103
- const store = context.configStore;
104
- if (!(store instanceof NoSqlConfigStore)) throw new TypeError('Tuya requires an encrypted NoSqlConfigStore and caller-owned encryption key provider.');
105
- const scope = store.getScope();
106
- if (scope.homeId !== configArg.homeId || scope.integrationDomain !== 'tuya') throw new TypeError('Tuya encrypted store must belong to the exact configured home and integration.');
107
- if (context.abortSignal !== undefined && !(context.abortSignal instanceof AbortSignal)) throw new TypeError('Tuya setup cancellation must be an AbortSignal.');
108
- return store;
103
+ tuyaKeys(context, ['homeId', 'configStore', 'abortSignal', 'logger']);
104
+ if (!(context.configStore instanceof NoSqlConfigStore)) throw new TypeError('Tuya requires an encrypted NoSqlConfigStore and caller-owned encryption key provider.');
105
+ return Object.freeze(context) as IIntegrationSetupContext;
109
106
  };
110
107
 
111
108
  export const tuyaCredentialId = (deviceIdArg: string): string => `device:${deviceIdArg}`;
@@ -1,35 +1,69 @@
1
- # Native LG webOS TV
1
+ # LG webOS TV
2
2
 
3
- `WebostvIntegration` and the exported `HomeAssistantWebostvIntegration` constructor speak the TV's documented SSAP WebSocket protocol. Setup verifies an explicitly configured TV UUID, registers, persists the returned pairing credential encrypted, and obtains complete initial power, volume, mute, foreground-app and input-list subscriptions before exposing a runtime.
3
+ The native runtime pairs with a verified TV through its SSAP WebSocket endpoint,
4
+ stores pairing keys encrypted in the host's database, and observes power, volume,
5
+ mute, foreground application and external inputs through authoritative subscriptions.
6
+ It uses the installed `ws` transport and requires no Home Assistant server.
7
+ `HomeAssistantWebostvIntegration` remains an exported constructor alias.
4
8
 
5
9
  ```typescript
6
- const integration = new WebostvIntegration({ db, keyProvider });
10
+ const integration = new WebostvIntegration({
11
+ db,
12
+ keyProvider,
13
+ allowedCommands: ['volume_set', 'volume_mute'],
14
+ });
7
15
  const runtime = await integration.setup({
8
- homeId: 'home-one',
9
- endpoint: 'wss://verified-tv.example:3001/',
16
+ homeId: 'home:example',
17
+ endpoint: 'wss://tv.example:3001/',
10
18
  expectedDeviceUUID: verifiedDeviceUUID,
11
19
  pairingMode: 'prompt',
12
- certificateAuthority: verifiedPemCertificateChain,
13
- }, {});
14
- const device = (await runtime.devices())[0];
15
- await runtime.callService!({
16
- domain: 'media_player', service: 'volume_set',
17
- target: { deviceId: device.id }, data: { volume: 25 },
20
+ certificateAuthority: ownerCertificateChain,
21
+ }, { homeId: 'home:example', abortSignal });
22
+
23
+ const [device] = await runtime.devices();
24
+ const result = await runtime.callService!({
25
+ domain: 'webostv',
26
+ service: 'volume_set',
27
+ target: { deviceId: device.id },
28
+ data: { volume: 25 },
18
29
  });
19
30
  await integration.destroy();
20
31
  ```
21
32
 
22
- The default registry's no-argument constructor also supports `setup(config, { configStore })`, where `configStore` is an actual `NoSqlConfigStore` whose public `getScope()` exactly matches the configured home and `webostv` domain. Generic stores and other scopes fail before connection; an invalid provided store never falls back to constructor options. A correctly scoped context store takes precedence over constructor persistence options.
33
+ The host independently selects `context.homeId`. The integration binds that home
34
+ once, including through config flow and failed setup. An optional constructor
35
+ `homeId` constrains the selected context. Configuration, discovery, form values
36
+ and database scope cannot select a home or grant control. Config flow captures
37
+ the selected home and rejects submitted home fields. Its forms stop accepting
38
+ submissions after flow close or integration destruction.
23
39
 
24
- The caller owns `db` and the external `keyProvider` (`currentKey(signal)` / `keyForId(id, signal)`, 32-byte keys). The integration constructs the existing `NoSqlConfigStore` in the immutable `homeId` / `webostv` scope. The pairing record also binds the exact endpoint and TV UUID. Accepted keys are durably stored before subscription setup; storage failure closes setup. There is no filesystem or memory-only credential fallback. `existing` mode rejects any prompt response and closes the connection; the TV may briefly display its prompt before that close. It never waits for or accepts fresh pairing in that mode. Explicit `prompt` mode permits a user to approve initial/rejected-key registration on the TV; a valid stored key is still reused. No key appears in device state, events, command receipts or errors.
40
+ The exported client has the same host boundary:
41
+ `new WebostvClient(config, { homeId, abortSignal }, options)`. Construction performs
42
+ no pairing, provider calls or transport I/O. Setup requires either constructor
43
+ `db` and `keyProvider` together, or an actual `NoSqlConfigStore` in the context
44
+ with the exact selected home and `webostv` domain. Scope checks call the genuine
45
+ store implementation; overriding `getScope` cannot grant ownership. Context
46
+ stores, databases and key providers remain caller-owned.
47
+ The provider supplies 32-byte keys through `currentKey(signal)` and
48
+ `keyForId(id, signal)`. A correctly scoped context store takes precedence over
49
+ constructor persistence; an invalid supplied store fails without falling back.
25
50
 
26
- Use the TV's independently verified UUID and endpoint; discovery supplies no network probes or identity verification by itself. Only explicit manual candidates with `integrationDomain: 'webostv'` and metadata `{ endpoint, expectedDeviceUUID }` are accepted. WSS uses normal certificate-chain and hostname validation, optionally with the supplied PEM CA chain. TLS verification is never disabled. A deliberately selected cleartext local `ws://...:3000/` endpoint requires `allowUnencryptedConnection: true`; its UUID check is a device selection check and provides no cryptographic protection against an attacker on that network. No automatic WS downgrade, redirects or endpoint guessing occur.
51
+ Every effect defaults to denied. The host supplies a copied, immutable
52
+ `allowedCommands` list in constructor options. The supported exact values are
53
+ `turn_on`, `turn_off`, `volume_set`, `volume_up`, `volume_down`, `volume_mute`,
54
+ `select_source`, `media_play`, `media_pause` and `media_stop`. Unknown, duplicate,
55
+ sparse or accessor-bearing lists are rejected. Saved configuration and form
56
+ values cannot set permissions. Volume and mute features are writable only while
57
+ connected and explicitly authorized. Service targets and feature writes must
58
+ refer to this runtime's home-prefixed device and media-player entity; input
59
+ selection requires an advertised input ID. Deep-standby wake-on-LAN and
60
+ next/previous track controls are unsupported.
27
61
 
28
- Supported exact-target services in `webostv` or `media_player`:
62
+ Services accept the `webostv` or `media_player` domain with these exact arguments:
29
63
 
30
- | Service | Exact data | SSAP endpoint |
64
+ | Service | Data | SSAP endpoint |
31
65
  | --- | --- | --- |
32
- | `status`, `snapshot` | none | Last authoritative subscribed state, with current connection availability |
66
+ | `status`, `snapshot` | none | Last authoritative subscribed state and current availability |
33
67
  | `volume_set` | `{ volume: integer 0..100 }` | `audio/setVolume` |
34
68
  | `volume_up`, `volume_down` | none | `audio/volumeUp`, `audio/volumeDown` |
35
69
  | `volume_mute` | `{ mute: boolean }` | `audio/setMute` |
@@ -37,12 +71,71 @@ Supported exact-target services in `webostv` or `media_player`:
37
71
  | `media_play`, `media_pause`, `media_stop` | none | `media.controls/play`, `/pause`, `/stop` |
38
72
  | `turn_on`, `turn_off` | none | `system/turnOn`, `system/turnOff` |
39
73
 
40
- Calls require the connected home-scoped device id or its exact media-player entity id. Foreign domains/devices/entities, unknown services, extra arguments, accessors, arbitrary input ids and out-of-range volume fail before command I/O. `writeFeature` accepts only the owned `volume` and `muted` features with validated values. Power state and foreground-app features are read only.
74
+ The TV must report the expected UUID before an existing pairing key is sent.
75
+ Stored keys are bound to both UUID and endpoint. `prompt` explicitly permits
76
+ pairing confirmation on the TV; `existing` refuses absent or rejected durable
77
+ pairing. TLS certificate validation stays enabled, and an owner-supplied PEM
78
+ chain can establish trust. Cleartext `ws` requires explicit
79
+ `allowUnencryptedConnection: true`. Credentials in endpoints, arbitrary URL
80
+ paths and fixture/injected-client production configs are rejected.
81
+ Discovery admits only explicit manual candidates with the domain and metadata
82
+ `{ endpoint, expectedDeviceUUID }`; it does not probe or verify a TV. Cleartext
83
+ UUID selection provides no cryptographic authentication. There is no automatic
84
+ WS downgrade, endpoint guessing, filesystem persistence or credential fallback.
85
+ The accepted key is stored before subscriptions start. Existing mode never
86
+ accepts fresh pairing, though the TV may display a prompt before disconnection.
87
+
88
+ A successful command receipt reports `acknowledgement: 'tv-response'` and
89
+ `physicalCompletion: 'unverified'`. Independent one-shot reads of the documented
90
+ state endpoints produce `observationStatus: 'observed'` and an observation when
91
+ available. Failure to read back an already accepted command produces
92
+ `observationStatus: 'unavailable'`; it never replays the command or substitutes
93
+ cached state as a fresh observation. Readback does not optimistically overwrite
94
+ subscription state. Model metadata comes from `system/getSystemInfo.modelName`
95
+ and stays absent when unreported. Owner permissions are not advertised as a
96
+ TV-reported per-command capability list.
97
+ Power-off timeout remains indeterminate even if the TV physically turned off.
98
+ `turn_on` requires an already connected TV supporting that method. Media controls
99
+ depend on the foreground application. All five state subscriptions and the
100
+ pre-registration system-info request are required; unsupported firmware fails
101
+ setup explicitly. Remote pointer sockets, app launching, channels and calibration
102
+ remain outside the runtime.
103
+
104
+ The owner defaults to a combined capacity of 16 pending operations, runtimes and
105
+ retained cleanup resources. Runtime destruction releases its setup lease.
106
+ Integration destruction is terminal and shares one close promise. The client
107
+ publishes its close promise before cancellation and availability callbacks, then
108
+ drains startup, commands, accepted async subscribers and owned store cleanup.
109
+ Unsubscribe removes admission and joins callbacks already accepted for that
110
+ subscription. Throwing or rejecting subscribers are isolated and removed.
111
+ Callbacks can request teardown; directly returning the client, runtime or owner's close promise
112
+ does not create a self-wait. A callback must not await teardown that is itself
113
+ waiting for that callback to finish.
114
+
115
+ Late encryption-provider cleanup failures reject close and remain retained.
116
+ After the provider settles, explicit `retryCleanup()` acknowledges cleanup
117
+ without replaying provider calls, pairing or commands, and without reopening
118
+ admission. A client closes only stores it created; borrowed stores remain usable.
119
+ Frames, pending requests, subscribers and inputs remain bounded. Heartbeat or
120
+ malformed authoritative state closes the instance. Disconnected snapshots retain
121
+ their observation timestamp and report unavailable. Reconnection requires a new
122
+ setup; there is no surviving command queue.
41
123
 
42
- Success means a correlated positive TV response, not verified physical completion. State changes only from validated subscription responses. A timeout, cancellation or disconnect after dispatch has an indeterminate effect outcome and is never replayed automatically. TV power-off responses are sometimes absent: that is reported as indeterminate even if the TV turns off. `turn_on` requires an already connected TV that supports that SSAP method; deep standby wake-on-LAN is not implemented. Media commands depend on the current TV application, and vendor rejections fail. Next/previous track, remote pointer sockets, app launching, channels and calibration are outside this runtime.
124
+ Migration requires an independently selected home in setup, flow and client
125
+ contexts, and explicit constructor command permissions for effects. The former
126
+ client `(config, persistenceOptions, signal)` signature becomes
127
+ `(config, { homeId, configStore?, abortSignal? }, options)`. Production `rawData`,
128
+ `snapshot`, `snapshotProvider`, injected clients/command executors and fixture
129
+ mapping remain unsupported; fixture data belongs only in tests.
43
130
 
44
- Connection, pairing and requests have bounded deadlines; frames, pending requests, inputs and subscribers have limits. Heartbeat failure closes the instance. Malformed authoritative responses fail the connection; disconnected snapshots retain their observation timestamp and report unavailable. Caller abort closes setup; `destroy()` rejects pending requests, drains current encrypted-storage work and closes the owned socket. Initialization uses the config store's bounded, cancellable collection preparation. Constructor-created config stores are closed and drained on destroy; injected context stores and the caller's database remain open. Throwing event subscribers are removed without interrupting other subscribers. Reconnection requires a new setup; no command queue survives connection loss.
131
+ Protocol and transport references:
45
132
 
46
- Breaking migration: `rawData`, `snapshot`, `snapshotProvider`, injected `client` / `commandExecutor`, generic host/transport/path configuration and `WebostvMapper.toSnapshot*` fixture mapping were removed from the production webOS config. Supply the explicit native fields and constructor persistence owner above. Existing fixture-only test data belongs in a local protocol fixture, not application configuration. No existing fixture config is automatically converted because it does not prove a TV's identity or provide durable pairing credentials.
133
+ - [SSAP endpoints in aiowebostv](https://github.com/home-assistant-libs/aiowebostv/blob/main/aiowebostv/endpoints.py)
134
+ - [ConnectSDK webOS service](https://github.com/ConnectSDK/Connect-SDK-Android-Core/blob/master/src/com/connectsdk/service/WebOSTVService.java)
135
+ - [ws transport documentation](https://github.com/websockets/ws/blob/master/doc/ws.md)
136
+ - [ws on npm](https://www.npmjs.com/package/ws)
47
137
 
48
- Protocol sources: [LG Connect SDK socket registration and hello](https://github.com/ConnectSDK/Connect-SDK-Android-Core/blob/master/src/com/connectsdk/service/webos/WebOSTVServiceSocketClient.java), [LG SSAP control implementations](https://github.com/ConnectSDK/Connect-SDK-Android-Core/blob/master/src/com/connectsdk/service/WebOSTVService.java), [Home Assistant's maintained SSAP endpoints](https://github.com/home-assistant-libs/aiowebostv/blob/main/aiowebostv/endpoints.py), [pre-registration system-info sequence and state subscriptions](https://github.com/home-assistant-libs/aiowebostv/blob/main/aiowebostv/webos_client.py), [ws transport API](https://github.com/websockets/ws/blob/master/doc/ws.md). Registration uses a neutral unsigned permission manifest, not LG's test identity/signatures. Hardware pairing/firmware behavior was not verified against an owner's TV; local real WebSocket fixtures cover the documented exchange. Setup currently requires the documented pre-registration system-info request and all five state subscriptions; unsupported firmware fails explicitly.
138
+ Verification uses local WebSocket/TLS and disposable in-memory database fixtures.
139
+ Real pairing, firmware acceptance and owner-device control require separate owner
140
+ authorization. No TV-reported per-command service-list contract was confirmed;
141
+ the adapter does not invent one.