@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
@@ -2,6 +2,7 @@ import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
2
2
  import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
3
3
  import type { IEsphomeManualEntry, IEsphomeMdnsRecord } from './esphome.types.js';
4
4
  import { esphomeDefaultNativeApiPort } from './esphome.types.js';
5
+ import { esphomeMac } from './esphome.validation.js';
5
6
 
6
7
  const esphomeMdnsTypes = new Set(['_esphomelib._tcp.local', '_esphome._tcp.local']);
7
8
 
@@ -37,7 +38,6 @@ export class EsphomeMdnsMatcher implements IDiscoveryMatcher<IEsphomeMdnsRecord>
37
38
  metadata: {
38
39
  mdnsName: recordArg.name,
39
40
  mdnsType: recordArg.type,
40
- txt,
41
41
  deviceName,
42
42
  encryptionRequired: Boolean(this.txt(txt, 'api_encryption')),
43
43
  },
@@ -63,13 +63,8 @@ export class EsphomeMdnsMatcher implements IDiscoveryMatcher<IEsphomeMdnsRecord>
63
63
  }
64
64
 
65
65
  private normalizeMac(valueArg?: string): string | undefined {
66
- if (!valueArg) {
67
- return undefined;
68
- }
69
- const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
70
- if (compact.length !== 12) {
71
- return valueArg.toLowerCase();
72
- }
66
+ const compact = esphomeMac(valueArg);
67
+ if (!compact) return undefined;
73
68
  return compact.match(/.{1,2}/g)?.join(':');
74
69
  }
75
70
  }
@@ -101,11 +96,7 @@ export class EsphomeManualMatcher implements IDiscoveryMatcher<IEsphomeManualEnt
101
96
  manufacturer: inputArg.manufacturer || 'ESPHome',
102
97
  model: inputArg.model || 'Native API device',
103
98
  metadata: {
104
- ...inputArg.metadata,
105
99
  deviceName: inputArg.deviceName,
106
- password: inputArg.password,
107
- encryptionKey: inputArg.encryptionKey,
108
- noisePsk: inputArg.noisePsk,
109
100
  },
110
101
  },
111
102
  };
@@ -1,6 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
3
- import type { IEsphomeAreaInfo, IEsphomeClientCommand, IEsphomeConfig, IEsphomeDeviceInfo, IEsphomeEntityDescriptor, IEsphomeEntityState, IEsphomeManualEntry, IEsphomeSnapshot } from './esphome.types.js';
3
+ import type { IEsphomeAreaInfo, IEsphomeClientCommand, IEsphomeMappingConfig, IEsphomeDeviceInfo, IEsphomeEntityDescriptor, IEsphomeEntityState, IEsphomeManualEntry, IEsphomeSnapshot } from './esphome.types.js';
4
4
  import { esphomeDefaultNativeApiPort } from './esphome.types.js';
5
5
 
6
6
  const serviceDataFields = new Map<string, readonly string[]>([
@@ -18,11 +18,12 @@ const serviceDataFields = new Map<string, readonly string[]>([
18
18
  ]);
19
19
 
20
20
  export class EsphomeMapper {
21
- public static toSnapshot(configArg: IEsphomeConfig, connectedArg?: boolean, eventsArg: IEsphomeSnapshot['events'] = []): IEsphomeSnapshot {
21
+ public static toSnapshot(configArg: IEsphomeMappingConfig, connectedArg?: boolean, eventsArg: IEsphomeSnapshot['events'] = []): IEsphomeSnapshot {
22
22
  const source = configArg.snapshot;
23
23
  const manualEntry = configArg.manualEntries?.[0];
24
24
  const deviceInfo = this.deviceInfoFromConfig(configArg, source?.deviceInfo, manualEntry);
25
25
  return {
26
+ homeId: configArg.homeId ?? source?.homeId,
26
27
  host: configArg.host || source?.host || manualEntry?.host || deviceInfo.host,
27
28
  port: configArg.port || source?.port || manualEntry?.port || deviceInfo.port || esphomeDefaultNativeApiPort,
28
29
  connected: connectedArg ?? source?.connected ?? false,
@@ -216,7 +217,7 @@ export class EsphomeMapper {
216
217
  return `esphome_${this.slug(`${deviceIdentifier}_${entityArg.platform}_${objectId}_${entityArg.key}_${subDevice ?? 0}`)}`;
217
218
  }
218
219
 
219
- private static deviceInfoFromConfig(configArg: IEsphomeConfig, sourceArg?: IEsphomeDeviceInfo, manualEntryArg?: IEsphomeManualEntry): IEsphomeDeviceInfo {
220
+ private static deviceInfoFromConfig(configArg: IEsphomeMappingConfig, sourceArg?: IEsphomeDeviceInfo, manualEntryArg?: IEsphomeManualEntry): IEsphomeDeviceInfo {
220
221
  return {
221
222
  ...sourceArg,
222
223
  ...configArg.deviceInfo,
@@ -226,12 +227,12 @@ export class EsphomeMapper {
226
227
  model: configArg.deviceInfo?.model || sourceArg?.model || manualEntryArg?.model,
227
228
  host: configArg.host || sourceArg?.host || manualEntryArg?.host || configArg.deviceInfo?.host,
228
229
  port: configArg.port || sourceArg?.port || manualEntryArg?.port || configArg.deviceInfo?.port || esphomeDefaultNativeApiPort,
229
- usesPassword: configArg.deviceInfo?.usesPassword ?? sourceArg?.usesPassword ?? Boolean(configArg.password || manualEntryArg?.password),
230
- apiEncryptionSupported: configArg.deviceInfo?.apiEncryptionSupported ?? sourceArg?.apiEncryptionSupported ?? Boolean(configArg.encryptionKey || configArg.noisePsk || manualEntryArg?.encryptionKey || manualEntryArg?.noisePsk),
230
+ usesPassword: configArg.deviceInfo?.usesPassword ?? sourceArg?.usesPassword,
231
+ apiEncryptionSupported: configArg.deviceInfo?.apiEncryptionSupported ?? sourceArg?.apiEncryptionSupported,
231
232
  };
232
233
  }
233
234
 
234
- private static normalizeStates(statesArg: IEsphomeConfig['states'] | IEsphomeSnapshot['states'] | undefined): IEsphomeEntityState[] {
235
+ private static normalizeStates(statesArg: IEsphomeMappingConfig['states'] | IEsphomeSnapshot['states'] | undefined): IEsphomeEntityState[] {
235
236
  if (!statesArg) {
236
237
  return [];
237
238
  }
@@ -450,7 +451,8 @@ export class EsphomeMapper {
450
451
  }
451
452
 
452
453
  private static mainDeviceId(snapshotArg: IEsphomeSnapshot): string {
453
- return `esphome.device.${this.slug(this.compactDeviceIdentifier(snapshotArg.deviceInfo) || snapshotArg.deviceInfo.name || snapshotArg.host || 'configured')}`;
454
+ const scope = snapshotArg.homeId ? `home.${encodeURIComponent(snapshotArg.homeId)}.` : '';
455
+ return `${scope}esphome.device.${this.slug(this.compactDeviceIdentifier(snapshotArg.deviceInfo) || snapshotArg.deviceInfo.name || snapshotArg.host || 'configured')}`;
454
456
  }
455
457
 
456
458
  private static subDeviceId(snapshotArg: IEsphomeSnapshot, subDeviceIdArg: string | number): string {
@@ -1,4 +1,5 @@
1
1
  import type { TEntityPlatform } from '../../core/types.js';
2
+ import type * as plugins from '../../plugins.js';
2
3
 
3
4
  export const esphomeDefaultNativeApiPort = 6053;
4
5
 
@@ -16,13 +17,43 @@ export type TEsphomeEntityPlatform =
16
17
  | 'water_heater';
17
18
 
18
19
  export interface IEsphomeConfig {
20
+ readonly homeId: string;
21
+ readonly host: string;
22
+ readonly port?: number;
23
+ readonly deviceName: string;
24
+ readonly expectedMacAddress: string;
25
+ readonly expectedModel?: string;
26
+ readonly name?: string;
27
+ readonly credentialKey?: string;
28
+ readonly credentialSource?: 'provider' | 'store';
29
+ readonly initialSnapshotTimeoutMs?: number;
30
+ readonly commandTimeoutMs?: number;
31
+ }
32
+
33
+ /** Only the published native client's Noise option is supported; legacy passwords are not. */
34
+ export type TEsphomeCredentials = Readonly<Pick<plugins.esphomeNativeApi.EspHomeClientOptions, 'psk'>>;
35
+
36
+ export interface IEsphomeCredentialRequest {
37
+ readonly capturedHomeId: string;
38
+ readonly credentialKey: string;
39
+ readonly signal: AbortSignal;
40
+ /** Absolute milliseconds on performance.now(), including provisioning and native setup. */
41
+ readonly monotonicDeadline: number;
42
+ }
43
+
44
+ export interface IEsphomeIntegrationOptions {
45
+ readonly homeId?: string;
46
+ readonly capacity?: number;
47
+ readonly resolveCredentials?: (requestArg: IEsphomeCredentialRequest) => Promise<TEsphomeCredentials>;
48
+ }
49
+
50
+ /** Pure mapper input. This cannot be used for live setup. */
51
+ export interface IEsphomeMappingConfig {
52
+ homeId?: string;
19
53
  host?: string;
20
54
  port?: number;
21
55
  name?: string;
22
56
  deviceName?: string;
23
- password?: string;
24
- encryptionKey?: string;
25
- noisePsk?: string;
26
57
  initialSnapshotTimeoutMs?: number;
27
58
  commandTimeoutMs?: number;
28
59
  snapshot?: IEsphomeSnapshot;
@@ -151,6 +182,7 @@ export interface IEsphomeEntityState {
151
182
  }
152
183
 
153
184
  export interface IEsphomeSnapshot {
185
+ homeId?: string;
154
186
  host?: string;
155
187
  port: number;
156
188
  connected: boolean;
@@ -236,9 +268,6 @@ export interface IEsphomeManualEntry {
236
268
  deviceName?: string;
237
269
  model?: string;
238
270
  manufacturer?: string;
239
- password?: string;
240
- encryptionKey?: string;
241
- noisePsk?: string;
242
271
  metadata?: Record<string, unknown>;
243
272
  }
244
273
 
@@ -0,0 +1,66 @@
1
+ import type { IEsphomeConfig, TEsphomeCredentials } from './esphome.types.js';
2
+
3
+ export const esphomeData = (valueArg: unknown, fieldsArg: readonly string[], labelArg: string): Record<string, unknown> => {
4
+ if (!valueArg || typeof valueArg !== 'object' || (Object.getPrototypeOf(valueArg) !== Object.prototype && Object.getPrototypeOf(valueArg) !== null)) {
5
+ throw new TypeError(`${labelArg} must be a plain data object.`);
6
+ }
7
+ const captured: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
8
+ for (const key of Reflect.ownKeys(valueArg)) {
9
+ const field = Object.getOwnPropertyDescriptor(valueArg, key);
10
+ if (typeof key !== 'string' || !fieldsArg.includes(key) || !field || !field.enumerable || !('value' in field)) {
11
+ throw new TypeError(`${labelArg} contains an unsupported field or accessor.`);
12
+ }
13
+ captured[key] = field.value as unknown;
14
+ }
15
+ return captured;
16
+ };
17
+
18
+ export const esphomeIdentifier = (valueArg: unknown, labelArg: string): string => {
19
+ if (typeof valueArg !== 'string' || !valueArg || valueArg.trim() !== valueArg || valueArg.length > 256 || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
20
+ throw new TypeError(`${labelArg} must be a non-empty identifier without surrounding whitespace or control characters.`);
21
+ }
22
+ return valueArg;
23
+ };
24
+
25
+ export const esphomeTimeout = (valueArg: unknown, defaultArg: number): number => {
26
+ const value = valueArg ?? defaultArg;
27
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 2147483647) {
28
+ throw new TypeError('ESPHome timeouts must be integers from 1 through 2147483647.');
29
+ }
30
+ return value;
31
+ };
32
+
33
+ export const esphomeMac = (valueArg: unknown): string | undefined => {
34
+ if (typeof valueArg !== 'string' || !/^(?:[a-f\d]{12}|(?:[a-f\d]{2}:){5}[a-f\d]{2})$/i.test(valueArg)) return undefined;
35
+ return valueArg.replace(/:/g, '').toLowerCase();
36
+ };
37
+
38
+ export const captureEsphomeConfig = (valueArg: IEsphomeConfig): IEsphomeConfig => {
39
+ const value = esphomeData(valueArg, ['homeId', 'host', 'port', 'name', 'deviceName', 'expectedMacAddress', 'expectedModel', 'credentialKey', 'credentialSource', 'initialSnapshotTimeoutMs', 'commandTimeoutMs'], 'ESPHome configuration');
40
+ const host = esphomeIdentifier(value.host, 'ESPHome host');
41
+ if (/\s/.test(host)) throw new TypeError('ESPHome host must not contain whitespace.');
42
+ const port = value.port ?? 6053;
43
+ if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) throw new TypeError('ESPHome port must be an integer from 1 through 65535.');
44
+ const mac = esphomeMac(value.expectedMacAddress);
45
+ if (!mac) throw new TypeError('ESPHome expectedMacAddress must be a complete MAC address.');
46
+ if (value.credentialSource !== undefined && value.credentialSource !== 'provider' && value.credentialSource !== 'store') throw new TypeError('ESPHome credential source must be provider or store.');
47
+ if (value.credentialSource !== undefined && value.credentialKey === undefined) throw new TypeError('ESPHome credential source requires a credential reference.');
48
+ return Object.freeze({
49
+ homeId: esphomeIdentifier(value.homeId, 'ESPHome configuration homeId'), host, port,
50
+ deviceName: esphomeIdentifier(value.deviceName, 'ESPHome selected device name'), expectedMacAddress: mac,
51
+ expectedModel: value.expectedModel === undefined ? undefined : esphomeIdentifier(value.expectedModel, 'ESPHome selected model'),
52
+ name: value.name === undefined ? undefined : esphomeIdentifier(value.name, 'ESPHome display name'),
53
+ credentialKey: value.credentialKey === undefined ? undefined : esphomeIdentifier(value.credentialKey, 'ESPHome credential reference'),
54
+ credentialSource: value.credentialSource as 'provider' | 'store' | undefined,
55
+ initialSnapshotTimeoutMs: esphomeTimeout(value.initialSnapshotTimeoutMs, 15000), commandTimeoutMs: esphomeTimeout(value.commandTimeoutMs, 5000),
56
+ });
57
+ };
58
+
59
+ export const captureEsphomeCredentials = (valueArg: unknown): TEsphomeCredentials => {
60
+ const value = esphomeData(valueArg, ['psk'], 'ESPHome credential provider result');
61
+ const psk = value.psk;
62
+ if (psk !== undefined && psk !== null && (typeof psk !== 'string' || !/^[A-Za-z0-9+/]{43}=$/.test(psk) || Buffer.from(psk, 'base64').length !== 32 || Buffer.from(psk, 'base64').toString('base64') !== psk)) {
63
+ throw new TypeError('ESPHome Noise credentials require a canonical base64-encoded 32-byte key.');
64
+ }
65
+ return Object.freeze({ psk: psk as string | null | undefined });
66
+ };
@@ -10,15 +10,25 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
10
10
 
11
11
  Install the parent package with `pnpm add @smarthome.exchange/integrations`. The native client requires Node.js 22.20 or newer and ESPHome 2025.10 or newer. Configure one native API node per integration instance; its advertised logical sub-devices share that connection.
12
12
 
13
- Provide a host and an optional port, which defaults to `6053`. For an encrypted device, supply its canonical base64-encoded 32-byte key as `encryptionKey` or `noisePsk`. Conflicting keys, legacy API passwords, injected snapshots, and command executors are rejected during live setup. When a key is supplied, a plaintext connection is rejected before controls become available.
13
+ Provide `homeId`, a host, the selected native `deviceName`, and `expectedMacAddress`; the optional port defaults to `6053`. An optional `expectedModel` must match the model in the actual native device-info response. Setup checks these observed identity fields before publishing readiness, including after reconnect. Discovery supplies endpoint hints and does not prove identity or select the owning home. Home and device/entity routing identifiers include the independently admitted home. MAC/name checks are selection constraints, not authentication; use Noise encryption for authenticated transport.
14
+
15
+ The embedding host must independently supply the same `homeId` in setup and config-flow contexts. An optional constructor home constrains this choice and never substitutes for context. The first valid admission binds an otherwise unbound integration, even when setup fails. Construction performs no network or persistence work. Shared ownership defaults to sixteen pending operations, live runtimes and retained cleanup resources.
16
+
17
+ Configuration contains only an optional `credentialKey` reference. Resolve it through the constructor's borrowed asynchronous `resolveCredentials` callback, which receives the captured home, reference, cancellation signal and an absolute `performance.now()` deadline. Its result supports the published client's `{ psk }` option only: a canonical base64-encoded 32-byte Noise key, or no key for a plaintext endpoint. The entire setup deadline includes credential provisioning. Cancellation joins the actual provider promise and rejects late results before transport construction; an uncooperative provider can therefore delay shutdown. Provider errors are replaced with fixed diagnostics to avoid exposing secrets.
14
18
 
15
19
  ```typescript
16
20
  import { EsphomeIntegration } from '@smarthome.exchange/integrations';
17
21
 
18
- async function inspectNode(host: string, encryptionKey: string) {
19
- const integration = new EsphomeIntegration();
22
+ async function inspectNode(homeId: string, host: string, macAddress: string, resolveNoiseKey: (reference: string, signal: AbortSignal) => Promise<string>) {
23
+ const integration = new EsphomeIntegration({
24
+ resolveCredentials: async ({ credentialKey, signal }) => ({ psk: await resolveNoiseKey(credentialKey, signal) }),
25
+ });
20
26
  try {
21
- const runtime = await integration.setup({ host, encryptionKey }, {
27
+ const runtime = await integration.setup({
28
+ homeId, host, deviceName: 'selected_node', expectedMacAddress: macAddress,
29
+ credentialKey: 'selected-node-noise',
30
+ }, {
31
+ homeId,
22
32
  abortSignal: AbortSignal.timeout(15_000),
23
33
  });
24
34
  return await runtime.devices();
@@ -28,7 +38,9 @@ async function inspectNode(host: string, encryptionKey: string) {
28
38
  }
29
39
  ```
30
40
 
31
- Obtain encryption keys through the application's secret provisioning flow. Discovery metadata does not prove a device's identity or grant authority to control it. The caller remains responsible for home access and tool authorization.
41
+ Obtain encryption keys through the application's secret provisioning flow. Forms accept references, and discovery never copies passwords, keys or arbitrary TXT/manual metadata. Raw `encryptionKey`, `noisePsk`, legacy passwords, injected snapshots and command executors are rejected in live configuration. A provisioned PSK session that negotiates plaintext is rejected before controls become available. The caller remains responsible for authentication, home access and tool authorization.
42
+
43
+ For an explicitly selected encrypted store reference, set `credentialSource: 'store'` and supply an initialized actual `NoSqlConfigStore` scoped to this home and the `esphome` domain. The caller provisions `{ psk }` under `credentialKey` through the store's public API. The default source is the external provider; neither source falls through to the other on failure. Store reads use the store's configured operation timeout and expose no per-call cancellation option; shutdown joins their actual completion and fences late results. The store, database and encryption-key provider remain caller-owned. No store is needed for an external provider or plaintext setup.
32
44
 
33
45
  ## State and controls
34
46
 
@@ -38,7 +50,9 @@ Readable platforms are lights, switches, fans, covers, climate entities, numbers
38
50
 
39
51
  Use `runtime.writeFeature` with a device and feature returned by that runtime, or `runtime.callService` with an explicit entity or device target. A device target must select exactly one supported entity. Supplying both targets requires them to name the same device. Foreign integration devices, unrelated service domains, out-of-range cover positions, and unknown service settings are rejected. Service data must contain ordinary values without accessors or symbol fields. The `esphome` service domain can select a supported native entity; platform domains must match the selected entity.
40
52
 
41
- Each entity admits one pending command. Success requires a subsequent matching state report within `commandTimeoutMs`, which defaults to 5 seconds. A matching report confirms the observed state; the protocol does not supply a unique action receipt or exactly-once physical execution guarantee. Timeout or cancellation can follow a physical effect, so failed or interrupted commands must not be retried automatically. Destruction disconnects the node, removes subscriptions, and cancels setup and pending confirmations.
53
+ Each entity admits one pending command. Success requires a subsequent matching state report within `commandTimeoutMs`, which defaults to 5 seconds. A matching report confirms the observed state; the protocol does not supply a unique action receipt or exactly-once physical execution guarantee. Timeout or cancellation can follow a physical effect, so failed or interrupted commands must not be retried automatically. Destruction publishes one stable terminal promise, aborts owned work, disconnects the node, drains pending confirmations and all subscriptions, and retains cleanup failures. `integration.retryCleanup()` retries failed cleanup explicitly without reconnecting or replaying commands. Destruction never closes borrowed credential or persistence resources.
54
+
55
+ The published SDK's synchronous `disconnect()` cancels reconnection, disposes its owned transport and reports disconnect/health transitions. Fixture peers observe the resulting TCP close for plaintext and Noise sessions. The SDK does not expose an OS socket-close completion promise, and this adapter makes no stronger OS-close or guaranteed memory-zeroization claim.
42
56
 
43
57
  Verification uses real plaintext and Noise-encrypted local TCP fixtures plus the published native client's encoder/decoder and `MockTransport`. Real hardware behavior has not been verified in this lane.
44
58