@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
@@ -1,48 +1,61 @@
1
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
2
+ import type { INativeAdmissionContext } from '../../core/classes.nativehomeadmission.js';
1
3
  import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
2
4
  import type { IMqttConfig } from './mqtt.types.js';
5
+ import { mqttRecord, normalizeMqttIntegrationConfig } from './mqtt.validation.js';
3
6
 
4
7
  export class MqttConfigFlow implements IConfigFlow<IMqttConfig> {
8
+ readonly #scope: NativeIntegrationScope;
9
+
10
+ constructor(ownerArg: NativeIntegrationOwner = new NativeIntegrationOwner('mqtt')) {
11
+ this.#scope = ownerArg.createFlowScope();
12
+ }
13
+
14
+ public destroy(): Promise<void> { return this.#scope.close(); }
15
+ public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
16
+
5
17
  public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IMqttConfig>> {
6
- void contextArg;
18
+ if (candidateArg.integrationDomain !== undefined && candidateArg.integrationDomain !== 'mqtt') return { kind: 'error', error: 'MQTT requires an MQTT candidate.' };
19
+ let context: INativeAdmissionContext;
20
+ try { context = this.#scope.admit(contextArg); }
21
+ catch { return { kind: 'error', error: 'MQTT configuration requires the explicit matching host home and an open flow.' }; }
22
+ const host = typeof candidateArg.host === 'string' ? candidateArg.host : undefined;
7
23
  return {
8
- kind: 'form',
9
- title: 'Connect MQTT broker',
10
- description: 'Configure a local MQTT 3.1.1 broker and Home Assistant-style MQTT discovery.',
24
+ kind: 'form', title: 'Connect selected MQTT broker',
25
+ description: 'Use a caller-owned credential reference. Selected topic observations are read-only; discovery mode uses the configured MQTT discovery schema.',
11
26
  fields: [
12
- { name: 'host', label: 'Broker host', type: 'text', required: true },
27
+ { name: 'host', label: 'Selected broker host', type: 'text', required: true },
13
28
  { name: 'port', label: 'Broker port', type: 'number' },
14
- { name: 'protocol', label: 'Protocol', type: 'select', options: [{ label: 'MQTT', value: 'mqtt' }, { label: 'MQTTS', value: 'mqtts' }] },
15
- { name: 'username', label: 'Username', type: 'text' },
16
- { name: 'password', label: 'Password', type: 'password' },
29
+ { name: 'protocol', label: 'Protocol', type: 'select', options: [{ label: 'MQTTS', value: 'mqtts' }, { label: 'MQTT', value: 'mqtt' }] },
30
+ { name: 'mode', label: 'Mapping mode', type: 'select', options: [{ label: 'Selected topics', value: 'selected-topics' }, { label: 'MQTT discovery', value: 'discovery' }] },
31
+ { name: 'mappings', label: 'Explicit observation mappings', type: 'text' },
32
+ { name: 'credentialKey', label: 'Caller credential reference', type: 'text' },
33
+ { name: 'allowUnencryptedLan', label: 'Consent to unencrypted local MQTT', type: 'boolean' },
17
34
  { name: 'clientId', label: 'Client ID', type: 'text' },
18
35
  { name: 'discoveryPrefix', label: 'Discovery prefix', type: 'text' },
19
36
  ],
20
- submit: async (valuesArg) => {
21
- const host = this.stringValue(valuesArg.host) || candidateArg.host;
22
- const protocol = valuesArg.protocol === 'mqtts' ? 'mqtts' : 'mqtt';
23
- const port = valuesArg.port ?? candidateArg.port ?? (protocol === 'mqtts' ? 8883 : 1883);
24
- if (!host || typeof port !== 'number' || !Number.isSafeInteger(port) || port < 1 || port > 65535) {
25
- return { kind: 'error', error: 'A broker host and integer port from 1 through 65535 are required.' };
26
- }
27
- return {
28
- kind: 'done',
29
- title: 'MQTT configured',
30
- config: {
31
- host,
32
- port,
33
- protocol,
34
- username: this.stringValue(valuesArg.username),
35
- password: typeof valuesArg.password === 'string' ? valuesArg.password : undefined,
36
- clientId: this.stringValue(valuesArg.clientId),
37
- discovery: true,
38
- discoveryPrefix: this.stringValue(valuesArg.discoveryPrefix) || 'homeassistant',
39
- },
40
- };
41
- },
37
+ submit: (valuesArg) => this.#submit(valuesArg, context, host),
42
38
  };
43
39
  }
44
40
 
45
- private stringValue(valueArg: unknown): string | undefined {
46
- return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
41
+ async #submit(valuesArg: Record<string, unknown>, contextArg: INativeAdmissionContext, candidateHostArg?: string): Promise<IConfigFlowStep<IMqttConfig>> {
42
+ let config: IMqttConfig & { homeId: string };
43
+ try {
44
+ const values = mqttRecord(valuesArg, ['homeId', 'host', 'port', 'protocol', 'mode', 'mappings', 'credentialKey',
45
+ 'allowUnencryptedLan', 'clientId', 'discoveryPrefix', 'connectTimeoutMs', 'operationTimeoutMs']);
46
+ if (values.homeId !== undefined && values.homeId !== contextArg.homeId) throw new TypeError('MQTT form home conflicts with the host home.');
47
+ const mappings = typeof values.mappings === 'string' ? JSON.parse(values.mappings) as unknown : values.mappings;
48
+ config = normalizeMqttIntegrationConfig({ ...values, mappings, host: values.host ?? candidateHostArg,
49
+ protocol: values.protocol ?? 'mqtts', mode: values.mode ?? 'discovery', homeId: contextArg.homeId });
50
+ } catch { return { kind: 'error', error: 'MQTT values, exact topic mappings or host home are invalid. Supply credential references rather than secrets.' }; }
51
+ try {
52
+ return await this.#scope.run(contextArg, config.homeId, async (leaseArg): Promise<IConfigFlowStep<IMqttConfig>> => {
53
+ leaseArg.assertOpen();
54
+ return { kind: 'done', title: 'MQTT reference configuration prepared', config };
55
+ });
56
+ } catch (error) {
57
+ if (error instanceof AggregateError) throw error;
58
+ return { kind: 'error', error: 'MQTT configuration flow is closed or cancelled.' };
59
+ }
47
60
  }
48
61
  }
@@ -1,18 +1,23 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
3
+ import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
3
4
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
4
5
  import { MqttClient } from './mqtt.classes.client.js';
5
6
  import { MqttConfigFlow } from './mqtt.classes.configflow.js';
6
7
  import { createMqttDiscoveryDescriptor } from './mqtt.discovery.js';
7
8
  import { MqttMapper } from './mqtt.mapper.js';
8
- import type { IMqttConfig, IMqttPublishOptions } from './mqtt.types.js';
9
+ import { MqttSession } from './mqtt.classes.session.js';
10
+ import { normalizeMqttIntegrationConfig } from './mqtt.validation.js';
11
+ import type { IMqttConfig, IMqttIntegrationOptions, IMqttPublishOptions } from './mqtt.types.js';
9
12
 
10
13
  export class MqttIntegration extends BaseIntegration<IMqttConfig> {
11
14
  public readonly domain = 'mqtt';
12
15
  public readonly displayName = 'MQTT';
13
16
  public readonly status = 'control-runtime' as const;
14
17
  public readonly discoveryDescriptor = createMqttDiscoveryDescriptor();
15
- public readonly configFlow = new MqttConfigFlow();
18
+ public readonly configFlow: MqttConfigFlow;
19
+ readonly #owner: NativeIntegrationOwner;
20
+ readonly #options: IMqttIntegrationOptions;
16
21
  public readonly metadata = {
17
22
  source: 'home-assistant/core',
18
23
  upstreamPath: 'homeassistant/components/mqtt',
@@ -22,20 +27,31 @@ export class MqttIntegration extends BaseIntegration<IMqttConfig> {
22
27
  qualityScale: 'platinum',
23
28
  };
24
29
 
30
+ constructor(optionsArg: IMqttIntegrationOptions = {}) {
31
+ super();
32
+ this.#owner = new NativeIntegrationOwner('mqtt', { homeId: optionsArg.homeId, capacity: optionsArg.capacity });
33
+ this.#options = Object.freeze({ resolveCredentials: optionsArg.resolveCredentials });
34
+ this.configFlow = new MqttConfigFlow(this.#owner);
35
+ }
36
+
25
37
  public async setup(configArg: IMqttConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
26
- const client = new MqttClient(configArg);
27
- const runtime = new MqttRuntime(client);
28
- try {
29
- await client.start(contextArg.abortSignal);
38
+ const config = normalizeMqttIntegrationConfig(configArg);
39
+ return this.#owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
40
+ const session = new MqttSession(config, leaseArg, this.#options.resolveCredentials);
41
+ let runtime: MqttRuntime | undefined;
42
+ let cleanupAttempts = 0;
43
+ const closeSession = (): Promise<void> => cleanupAttempts++ === 0 ? session.close() : session.retryCleanup();
44
+ leaseArg.own(() => runtime ? runtime.cleanup() : closeSession());
45
+ const client = await session.start();
46
+ leaseArg.assertOpen();
47
+ runtime = new MqttRuntime(client, config, leaseArg, closeSession);
48
+ leaseArg.promoteToRuntime();
30
49
  return runtime;
31
- } catch (error) {
32
- try { await runtime.destroy(); }
33
- catch (cleanupError) { throw new AggregateError([error, cleanupError], 'MQTT setup and cleanup failed.'); }
34
- throw error;
35
- }
50
+ });
36
51
  }
37
52
 
38
- public async destroy(): Promise<void> {}
53
+ public destroy(): Promise<void> { return this.#owner.close(); }
54
+ public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
39
55
  }
40
56
 
41
57
  export class HomeAssistantMqttIntegration extends MqttIntegration {}
@@ -51,25 +67,30 @@ class MqttRuntime implements IIntegrationRuntime {
51
67
  private updateRequested = false;
52
68
  private updating = false;
53
69
  private destroyed = false;
70
+ private readonly operations = new Set<Promise<unknown>>();
71
+ private readonly failures = new Set<unknown>();
72
+ private readonly ownerAbort = (): void => { this.destroyed = true; };
54
73
 
55
- constructor(private readonly client: MqttClient) {
74
+ constructor(private readonly client: MqttClient, private readonly config: IMqttConfig,
75
+ private readonly lease: INativeSetupLease, private readonly closeSession: () => Promise<void>) {
56
76
  this.removeMessageHandler = client.onMessage(() => this.queueUpdate());
57
77
  this.removeConnectionHandler = client.onConnectionState((stateArg) => {
58
78
  if (stateArg.error) this.emit({ type: 'error', integrationDomain: this.domain, data: { message: stateArg.error.message }, timestamp: Date.now() });
59
79
  this.queueUpdate();
60
80
  });
81
+ lease.signal.addEventListener('abort', this.ownerAbort, { once: true });
61
82
  }
62
83
 
63
84
  public isHealthy(): boolean {
64
- return this.client.isHealthy();
85
+ return !this.destroyed && this.client.isHealthy();
65
86
  }
66
87
 
67
88
  public async devices(abortSignalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
68
- return MqttMapper.toDevices(await this.client.getSnapshot(abortSignalArg));
89
+ return this.operation(async () => MqttMapper.toDevices(await this.client.getSnapshot(this.signal(abortSignalArg))));
69
90
  }
70
91
 
71
92
  public async entities(): Promise<IIntegrationEntity[]> {
72
- return MqttMapper.toEntities(await this.client.getSnapshot());
93
+ return this.operation(async () => MqttMapper.toEntities(await this.client.getSnapshot(this.signal())));
73
94
  }
74
95
 
75
96
  public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, abortSignalArg?: AbortSignal): Promise<() => Promise<void>> {
@@ -92,35 +113,64 @@ class MqttRuntime implements IIntegrationRuntime {
92
113
  }
93
114
 
94
115
  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
95
- try {
96
- let command: IMqttPublishOptions | undefined;
97
- if (requestArg.domain === 'mqtt') {
98
- if (requestArg.service !== 'publish') return { success: false, error: 'Unsupported MQTT broker service.' };
99
- command = this.publishOptionsFromRequest(requestArg);
100
- } else command = MqttMapper.commandForService(await this.client.getSnapshot(), requestArg);
101
- if (!command) return { success: false, error: 'MQTT service requires an available, unambiguous supported target and valid input.' };
102
- await this.client.publish(command);
103
- return { success: true };
104
- } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; }
116
+ if (this.config.mode === 'selected-topics') return { success: false, error: 'Selected MQTT topic mappings are read-only.' };
117
+ return this.operation(async () => {
118
+ try {
119
+ let command: IMqttPublishOptions | undefined;
120
+ if (requestArg.domain === 'mqtt') {
121
+ if (requestArg.service !== 'publish') return { success: false, error: 'Unsupported MQTT broker service.' };
122
+ command = this.publishOptionsFromRequest(requestArg);
123
+ } else command = MqttMapper.commandForService(await this.client.getSnapshot(), requestArg);
124
+ if (!command) return { success: false, error: 'MQTT service requires an available, unambiguous supported target and valid input.' };
125
+ await this.client.publish(command, this.signal());
126
+ return { success: true, data: { transportAccepted: true, brokerAcknowledged: command.qos === 1 || command.qos === 2, physicalOutcomeConfirmed: false } };
127
+ } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; }
128
+ });
105
129
  }
106
130
 
107
131
  public async writeFeature(requestArg: IDeviceFeatureWriteRequest, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
108
- try {
109
- const command = MqttMapper.commandForFeature(await this.client.getSnapshot(abortSignalArg), requestArg);
110
- if (!command) return { success: false, error: 'MQTT feature requires an available, unambiguous supported target and valid value.' };
111
- await this.client.publish(command, abortSignalArg);
112
- return { success: true };
113
- } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; }
132
+ if (this.config.mode === 'selected-topics') return { success: false, error: 'Selected MQTT topic mappings are read-only.' };
133
+ return this.operation(async () => {
134
+ try {
135
+ const signal = this.signal(abortSignalArg);
136
+ const command = MqttMapper.commandForFeature(await this.client.getSnapshot(signal), requestArg);
137
+ if (!command) return { success: false, error: 'MQTT feature requires an available, unambiguous supported target and valid value.' };
138
+ await this.client.publish(command, signal);
139
+ return { success: true, data: { transportAccepted: true, brokerAcknowledged: command.qos === 1 || command.qos === 2, physicalOutcomeConfirmed: false } };
140
+ } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; }
141
+ });
114
142
  }
115
143
 
116
- public async destroy(): Promise<void> {
144
+ public destroy(): Promise<void> { return this.lease.close(); }
145
+
146
+ public async cleanup(): Promise<void> {
117
147
  this.destroyed = true;
118
148
  this.removeMessageHandler();
119
149
  this.removeConnectionHandler();
120
- for (const cleanup of [...this.subscriptionCleanups]) await cleanup();
150
+ const cleanups = [...this.subscriptionCleanups];
121
151
  this.handlers.clear();
122
- await this.client.destroy();
123
- await this.updatePromise;
152
+ this.lease.signal.removeEventListener('abort', this.ownerAbort);
153
+ const results = await Promise.allSettled([
154
+ ...cleanups.map((cleanup) => Promise.resolve().then(cleanup)), this.closeSession(), this.updatePromise,
155
+ ...[...this.operations].map((operation) => operation.then(() => undefined, () => undefined)),
156
+ ]);
157
+ const failures = [...this.failures, ...results.flatMap((result) => result.status === 'rejected' ? [result.reason as unknown] : [])];
158
+ this.failures.clear();
159
+ if (failures.length) throw new AggregateError(failures, 'MQTT runtime cleanup failed; retry cleanup explicitly.');
160
+ }
161
+
162
+ private signal(signalArg?: AbortSignal): AbortSignal {
163
+ this.lease.assertOpen();
164
+ if (this.destroyed) throw new Error('MQTT runtime has been destroyed.');
165
+ return signalArg ? AbortSignal.any([this.lease.signal, signalArg]) : this.lease.signal;
166
+ }
167
+
168
+ private operation<TValue>(workArg: () => Promise<TValue>): Promise<TValue> {
169
+ this.signal();
170
+ const work = Promise.resolve().then(workArg);
171
+ this.operations.add(work);
172
+ void work.then(() => { this.operations.delete(work); }, () => { this.operations.delete(work); });
173
+ return work;
124
174
  }
125
175
 
126
176
  private queueUpdate(): void {
@@ -129,7 +179,16 @@ class MqttRuntime implements IIntegrationRuntime {
129
179
  if (this.updating) return;
130
180
  this.updating = true;
131
181
  this.updatePromise = this.updateSnapshots().catch((error: unknown) => {
132
- if (!this.destroyed) this.emit({ type: 'error', integrationDomain: this.domain, data: { message: error instanceof Error ? error.message : String(error) }, timestamp: Date.now() });
182
+ if (!this.destroyed || error instanceof AggregateError) this.failures.add(error);
183
+ if (!this.destroyed) {
184
+ try { this.emit({ type: 'error', integrationDomain: this.domain, data: { message: error instanceof Error ? error.message : String(error) }, timestamp: Date.now() }); }
185
+ catch (callbackError) { this.failures.add(callbackError); }
186
+ }
187
+ if (this.failures.size) {
188
+ this.destroyed = true;
189
+ // Stop admitting updates after a delivery failure; the retained lease owns reporting/retry.
190
+ void this.lease.close().catch(() => undefined);
191
+ }
133
192
  }).finally(() => {
134
193
  this.updating = false;
135
194
  if (this.updateRequested && !this.destroyed) this.queueUpdate();
@@ -158,7 +217,9 @@ class MqttRuntime implements IIntegrationRuntime {
158
217
  }
159
218
 
160
219
  private emit(eventArg: IIntegrationEvent): void {
161
- for (const handler of this.handlers) handler(eventArg);
220
+ const failures: unknown[] = [];
221
+ for (const handler of this.handlers) { try { handler(eventArg); } catch (error) { failures.push(error); } }
222
+ if (failures.length) throw new AggregateError(failures, 'MQTT event callbacks failed.');
162
223
  }
163
224
 
164
225
  private publishOptionsFromRequest(requestArg: IServiceCallRequest): IMqttPublishOptions | undefined {
@@ -0,0 +1,118 @@
1
+ import type { INativeOperationLease } from '../../core/classes.nativeintegrationowner.js';
2
+ import { MqttClient } from './mqtt.classes.client.js';
3
+ import type { IMqttConfig, IMqttTransportCredentials, TMqttCredentialResolver } from './mqtt.types.js';
4
+ import { mqttCredentials } from './mqtt.validation.js';
5
+
6
+ /** Owns actual caller credential work and the resulting transport, without owning the provider. */
7
+ export class MqttSession {
8
+ readonly #config: IMqttConfig & { homeId: string };
9
+ readonly #lease: INativeOperationLease;
10
+ readonly #resolver?: TMqttCredentialResolver;
11
+ readonly #lifetime = new AbortController();
12
+ readonly #signal: AbortSignal;
13
+ #provider?: Promise<IMqttTransportCredentials>;
14
+ #work?: Promise<MqttClient>;
15
+ #client?: MqttClient;
16
+ #closed = false;
17
+ #closePromise?: Promise<void>;
18
+ #retryPromise?: Promise<void>;
19
+ #providerDrained = false;
20
+ #transportClosed = false;
21
+ #transportCloseAttempted = false;
22
+
23
+ constructor(configArg: IMqttConfig & { homeId: string }, leaseArg: INativeOperationLease, resolverArg?: TMqttCredentialResolver) {
24
+ this.#config = configArg; this.#lease = leaseArg; this.#resolver = resolverArg;
25
+ this.#signal = AbortSignal.any([leaseArg.signal, this.#lifetime.signal]);
26
+ }
27
+
28
+ public start(): Promise<MqttClient> {
29
+ if (!this.#work) this.#work = Promise.resolve().then(() => this.#start());
30
+ return this.#work;
31
+ }
32
+
33
+ public close(): Promise<void> {
34
+ if (!this.#closePromise) {
35
+ this.#closed = true;
36
+ this.#closePromise = Promise.resolve().then(() => this.#drain());
37
+ this.#lifetime.abort(new Error('MQTT session is closed.'));
38
+ }
39
+ return this.#closePromise;
40
+ }
41
+
42
+ public retryCleanup(): Promise<void> {
43
+ if (!this.#closed) return Promise.reject(new Error('MQTT cleanup can only be retried after close.'));
44
+ if (!this.#retryPromise) {
45
+ const retry = Promise.resolve().then(() => this.#drain());
46
+ this.#retryPromise = retry;
47
+ void retry.then(() => { this.#retryPromise = undefined; }, () => { this.#retryPromise = undefined; });
48
+ }
49
+ return this.#retryPromise;
50
+ }
51
+
52
+ #assertOpen(): void {
53
+ this.#lease.assertOpen();
54
+ this.#signal.throwIfAborted();
55
+ if (this.#closed) throw new Error('MQTT session is closed.');
56
+ }
57
+
58
+ async #start(): Promise<MqttClient> {
59
+ this.#assertOpen();
60
+ const deadline = performance.now() + (this.#config.connectTimeoutMs ?? 7000);
61
+ let credentials: IMqttTransportCredentials = {};
62
+ if (this.#config.credentialKey) {
63
+ if (!this.#resolver) throw new Error('MQTT credential reference requires a caller resolver.');
64
+ const resolver = this.#resolver;
65
+ const request = Object.freeze({ homeId: this.#lease.context.homeId, credentialKey: this.#config.credentialKey,
66
+ signal: this.#signal, deadline });
67
+ // The microtask enters foreign code only after its actual promise is retained here.
68
+ this.#provider = Promise.resolve().then(() => {
69
+ this.#assertOpen();
70
+ return resolver(request);
71
+ }).then((value) => mqttCredentials(value), () => { throw new Error('MQTT caller credential resolution failed.'); });
72
+ const provider = this.#provider;
73
+ let timer: ReturnType<typeof setTimeout> | undefined;
74
+ let abort: () => void = () => {};
75
+ const cancellation = new Promise<never>((_resolve, reject) => {
76
+ abort = () => reject(this.#signal.reason as unknown);
77
+ timer = setTimeout(() => {
78
+ const error = new Error('MQTT credential resolution timed out.');
79
+ reject(error);
80
+ this.#lifetime.abort(error);
81
+ }, Math.max(1, deadline - performance.now()));
82
+ this.#signal.addEventListener('abort', abort, { once: true });
83
+ if (this.#signal.aborted) abort();
84
+ });
85
+ try { credentials = await Promise.race([provider, cancellation]); }
86
+ finally { if (timer) clearTimeout(timer); this.#signal.removeEventListener('abort', abort); }
87
+ }
88
+ this.#assertOpen();
89
+ const remaining = Math.floor(deadline - performance.now());
90
+ if (remaining < 1) throw new Error('MQTT setup timed out.');
91
+ const client = new MqttClient({ ...this.#config, connectTimeoutMs: remaining }, credentials);
92
+ this.#client = client;
93
+ // The session is already the lease's owned cleanup before this allocation and start.
94
+ try { await client.start(this.#signal); }
95
+ catch (error) { if (this.#signal.aborted) throw this.#signal.reason as unknown; throw error; }
96
+ this.#assertOpen();
97
+ return client;
98
+ }
99
+
100
+ async #drain(): Promise<void> {
101
+ const provider = this.#provider;
102
+ const client = this.#client;
103
+ const results = await Promise.allSettled([
104
+ ...(provider && !this.#providerDrained ? [provider.then(() => { this.#providerDrained = true; }, () => {
105
+ this.#providerDrained = true;
106
+ throw new Error('MQTT caller credential work failed while draining.');
107
+ })] : []),
108
+ ...(client && !this.#transportClosed ? [Promise.resolve().then(() => {
109
+ const retry = this.#transportCloseAttempted;
110
+ this.#transportCloseAttempted = true;
111
+ return retry ? client.retryCleanup() : client.destroy();
112
+ }).then(() => { this.#transportClosed = true; })] : []),
113
+ ...(this.#work ? [this.#work.then(() => undefined, () => undefined)] : []),
114
+ ]);
115
+ const failures = results.flatMap((result) => result.status === 'rejected' ? [result.reason as unknown] : []);
116
+ if (failures.length) throw new AggregateError(failures, 'MQTT session cleanup failed; retry cleanup explicitly.');
117
+ }
118
+ }
@@ -1,6 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
3
- import type { IMqttConfig, IMqttDeviceInfo, IMqttEntityConfig, IMqttMessage, IMqttPayload, IMqttPublishOptions, IMqttSnapshot } from './mqtt.types.js';
3
+ import type { IMqttConfig, IMqttDeviceInfo, IMqttEntityConfig, IMqttMessage, IMqttPayload, IMqttPublishOptions, IMqttSnapshot, IMqttTopicMapping } from './mqtt.types.js';
4
4
 
5
5
  const defaultDiscoveryPrefix = 'homeassistant';
6
6
  const defaultPayloadOn = 'ON';
@@ -68,6 +68,8 @@ export class MqttMapper {
68
68
  ...(configArg.discovery === false ? [] : this.entitiesFromDiscoveryMessages(messages, configArg.discoveryPrefix || defaultDiscoveryPrefix)),
69
69
  ];
70
70
  return {
71
+ homeId: configArg.homeId,
72
+ selectedMappings: configArg.mode === 'selected-topics' ? structuredClone(configArg.mappings ?? []) : undefined,
71
73
  brokerInfo: {
72
74
  host: configArg.host,
73
75
  port: configArg.port || (configArg.protocol === 'mqtts' ? 8883 : 1883),
@@ -82,6 +84,7 @@ export class MqttMapper {
82
84
  }
83
85
 
84
86
  public static toDevices(snapshotArg: IMqttSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
87
+ if (snapshotArg.selectedMappings) return this.selectedDevices(snapshotArg);
85
88
  const updatedAt = new Date().toISOString();
86
89
  const devices = new Map<string, plugins.shxInterfaces.data.IDeviceDefinition>();
87
90
  const brokerId = this.brokerDeviceId(snapshotArg);
@@ -145,6 +148,13 @@ export class MqttMapper {
145
148
  }
146
149
 
147
150
  public static toEntities(snapshotArg: IMqttSnapshot): IIntegrationEntity[] {
151
+ if (snapshotArg.selectedMappings) return snapshotArg.selectedMappings.map((mappingArg) => ({
152
+ id: mappingArg.entityId, uniqueId: mappingArg.entityId, integrationDomain: 'mqtt', deviceId: mappingArg.deviceId,
153
+ platform: 'sensor', name: mappingArg.name, state: this.selectedValue(snapshotArg, mappingArg.stateTopic),
154
+ attributes: { homeId: snapshotArg.homeId, stateTopic: mappingArg.stateTopic, availabilityTopic: mappingArg.availabilityTopic,
155
+ observationFormat: 'raw-utf8', identitySource: 'owner-mapping' },
156
+ available: this.selectedAvailable(snapshotArg, mappingArg),
157
+ }));
148
158
  return snapshotArg.entities.map((entityArg) => ({
149
159
  id: entityArg.defaultEntityId || `${this.toEntityPlatform(entityArg.platform)}.${this.slug(entityArg.uniqueId || entityArg.objectId || entityArg.name || 'mqtt')}`,
150
160
  uniqueId: this.uniqueId(entityArg),
@@ -168,6 +178,7 @@ export class MqttMapper {
168
178
  }
169
179
 
170
180
  public static commandForService(snapshotArg: IMqttSnapshot, requestArg: IServiceCallRequest): IMqttPublishOptions | undefined {
181
+ if (snapshotArg.selectedMappings) return undefined;
171
182
  const entity = this.findTargetEntity(snapshotArg, requestArg);
172
183
  if (entity && requestArg.target.deviceId && this.deviceIdForEntity(entity, snapshotArg) !== requestArg.target.deviceId) return undefined;
173
184
  if (!entity?.commandTopic || requestArg.domain !== entity.platform || !this.controlSupported(entity) || !this.entityAvailable(entity, snapshotArg)) {
@@ -209,6 +220,7 @@ export class MqttMapper {
209
220
  }
210
221
 
211
222
  public static commandForFeature(snapshotArg: IMqttSnapshot, requestArg: IDeviceFeatureWriteRequest): IMqttPublishOptions | undefined {
223
+ if (snapshotArg.selectedMappings) return undefined;
212
224
  const entities = snapshotArg.entities.filter((entityArg) =>
213
225
  this.deviceIdForEntity(entityArg, snapshotArg) === requestArg.device.id &&
214
226
  this.featureForEntity(entityArg).id === requestArg.feature.id
@@ -584,6 +596,36 @@ export class MqttMapper {
584
596
  return Array.isArray(valueArg) ? JSON.stringify(valueArg) : valueArg as plugins.shxInterfaces.data.TDeviceStateValue;
585
597
  }
586
598
 
599
+ private static selectedValue(snapshotArg: IMqttSnapshot, topicArg: string): string | null {
600
+ const value = snapshotArg.messages.find((messageArg) => messageArg.topic === topicArg)?.payload;
601
+ return typeof value === 'string' ? value : null;
602
+ }
603
+
604
+ private static selectedAvailable(snapshotArg: IMqttSnapshot, mappingArg: IMqttTopicMapping): boolean {
605
+ return snapshotArg.brokerInfo.connected && this.selectedValue(snapshotArg, mappingArg.stateTopic) !== null
606
+ && (!mappingArg.availabilityTopic
607
+ || this.selectedValue(snapshotArg, mappingArg.availabilityTopic) === (mappingArg.payloadAvailable ?? 'online'));
608
+ }
609
+
610
+ private static selectedDevices(snapshotArg: IMqttSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
611
+ const devices = new Map<string, plugins.shxInterfaces.data.IDeviceDefinition>();
612
+ for (const mapping of snapshotArg.selectedMappings ?? []) {
613
+ let device = devices.get(mapping.deviceId);
614
+ if (!device) {
615
+ device = { id: mapping.deviceId, name: mapping.deviceName, integrationDomain: 'mqtt', protocol: 'mqtt',
616
+ online: false, features: [], state: [], metadata: { homeId: snapshotArg.homeId, identitySource: 'owner-mapping',
617
+ brokerHost: snapshotArg.brokerInfo.host, physicalIdentityConfirmed: false } };
618
+ devices.set(mapping.deviceId, device);
619
+ }
620
+ device.online ||= this.selectedAvailable(snapshotArg, mapping);
621
+ device.features.push({ id: mapping.featureId, name: mapping.name, capability: 'sensor', readable: true, writable: false });
622
+ const message = snapshotArg.messages.find((itemArg) => itemArg.topic === mapping.stateTopic);
623
+ device.state.push({ featureId: mapping.featureId, value: this.selectedValue(snapshotArg, mapping.stateTopic),
624
+ updatedAt: new Date(message?.timestamp ?? Date.now()).toISOString() });
625
+ }
626
+ return [...devices.values()];
627
+ }
628
+
587
629
  private static stringValue(valueArg: unknown): string | undefined {
588
630
  return typeof valueArg === 'string' && valueArg ? valueArg : undefined;
589
631
  }
@@ -1,6 +1,13 @@
1
1
  import type { TEntityPlatform } from '../../core/types.js';
2
+ import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
2
3
 
3
4
  export interface IMqttConfig {
5
+ /** Required by MqttIntegration; raw MqttClient remains a reusable transport. */
6
+ homeId?: string;
7
+ mode?: 'discovery' | 'selected-topics';
8
+ credentialKey?: string;
9
+ allowUnencryptedLan?: boolean;
10
+ mappings?: IMqttTopicMapping[];
4
11
  host?: string;
5
12
  port?: number;
6
13
  protocol?: 'mqtt' | 'mqtts';
@@ -26,6 +33,49 @@ export interface IMqttConfig {
26
33
  maxBufferedBytes?: number;
27
34
  }
28
35
 
36
+ /** Owner-defined identity and exact topics; payloads are opaque UTF-8 observations. */
37
+ export interface IMqttTopicMapping {
38
+ deviceId: string;
39
+ entityId: string;
40
+ featureId: string;
41
+ name: string;
42
+ deviceName: string;
43
+ stateTopic: string;
44
+ availabilityTopic?: string;
45
+ payloadAvailable?: string;
46
+ }
47
+
48
+ export interface IMqttSelectedConfig extends IMqttConfig {
49
+ homeId: string;
50
+ mode: 'selected-topics';
51
+ mappings: IMqttTopicMapping[];
52
+ }
53
+
54
+ /** Runtime-only subset of the documented MQTT.js TCP/TLS options. No file paths. */
55
+ export interface IMqttTransportCredentials {
56
+ username?: string;
57
+ password?: string;
58
+ ca?: string;
59
+ cert?: string;
60
+ key?: string;
61
+ servername?: string;
62
+ }
63
+
64
+ export interface IMqttCredentialRequest {
65
+ readonly homeId: string;
66
+ readonly credentialKey: string;
67
+ readonly signal: AbortSignal;
68
+ /** Absolute performance.now() deadline. */
69
+ readonly deadline: number;
70
+ }
71
+
72
+ /** The caller owns retrieval, storage and cancellation of this borrowed provider. */
73
+ export type TMqttCredentialResolver = (requestArg: IMqttCredentialRequest) => Promise<IMqttTransportCredentials>;
74
+
75
+ export interface IMqttIntegrationOptions extends INativeIntegrationOwnerOptions {
76
+ readonly resolveCredentials?: TMqttCredentialResolver;
77
+ }
78
+
29
79
  export interface IMqttConnectionState {
30
80
  connected: boolean;
31
81
  error?: Error;
@@ -61,6 +111,8 @@ export interface IMqttSnapshot {
61
111
  brokerInfo: IMqttBrokerInfo;
62
112
  entities: IMqttEntityConfig[];
63
113
  messages: IMqttMessage[];
114
+ homeId?: string;
115
+ selectedMappings?: IMqttTopicMapping[];
64
116
  }
65
117
 
66
118
  export interface IMqttDeviceInfo {