@homebridge-plugins/homebridge-tuya 3.0.0-beta.0 → 3.0.0-beta.2

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 (341) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +166 -145
  3. package/config.schema.json +976 -11
  4. package/dist/{core → cloud/api}/TuyaOpenAPI.js +16 -7
  5. package/dist/cloud/api/TuyaOpenAPI.js.map +1 -0
  6. package/dist/{core → cloud/api}/TuyaOpenMQ.js +13 -11
  7. package/dist/cloud/api/TuyaOpenMQ.js.map +1 -0
  8. package/dist/cloud/device/TuyaCustomDeviceManager.js.map +1 -0
  9. package/dist/{device → cloud/device}/TuyaDevice.js +14 -1
  10. package/dist/cloud/device/TuyaDevice.js.map +1 -0
  11. package/dist/{device → cloud/device}/TuyaDeviceManager.js +12 -3
  12. package/dist/cloud/device/TuyaDeviceManager.js.map +1 -0
  13. package/dist/cloud/device/TuyaHomeDeviceManager.js.map +1 -0
  14. package/dist/config.js +1 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/local/CloudLocalReconciliation.js +180 -0
  17. package/dist/local/CloudLocalReconciliation.js.map +1 -0
  18. package/dist/local/DynamicChildDiscovery.js +154 -0
  19. package/dist/local/DynamicChildDiscovery.js.map +1 -0
  20. package/dist/local/LocalDevice.js +419 -0
  21. package/dist/local/LocalDevice.js.map +1 -0
  22. package/dist/local/LocalDeviceManager.js +981 -0
  23. package/dist/local/LocalDeviceManager.js.map +1 -0
  24. package/dist/local/TuyaDiscovery.js +248 -0
  25. package/dist/local/TuyaDiscovery.js.map +1 -0
  26. package/dist/local/ZigbeeEventDetection.js +213 -0
  27. package/dist/local/ZigbeeEventDetection.js.map +1 -0
  28. package/dist/local/ZigbeeGatewayDetection.js +109 -0
  29. package/dist/local/ZigbeeGatewayDetection.js.map +1 -0
  30. package/dist/local/config.js +6 -0
  31. package/dist/local/config.js.map +1 -0
  32. package/dist/local/protocol/ChildPayloadUtility.js +94 -0
  33. package/dist/local/protocol/ChildPayloadUtility.js.map +1 -0
  34. package/dist/local/protocol/Protocol.js +3 -0
  35. package/dist/local/protocol/Protocol.js.map +1 -0
  36. package/dist/local/protocol/ProtocolFactory.js +27 -0
  37. package/dist/local/protocol/ProtocolFactory.js.map +1 -0
  38. package/dist/local/protocol/ProtocolUtilities.js +329 -0
  39. package/dist/local/protocol/ProtocolUtilities.js.map +1 -0
  40. package/dist/local/protocol/ProtocolV31V32.js +60 -0
  41. package/dist/local/protocol/ProtocolV31V32.js.map +1 -0
  42. package/dist/local/protocol/ProtocolV33.js +52 -0
  43. package/dist/local/protocol/ProtocolV33.js.map +1 -0
  44. package/dist/local/protocol/ProtocolV34.js +104 -0
  45. package/dist/local/protocol/ProtocolV34.js.map +1 -0
  46. package/dist/local/protocol/ProtocolV35.js +77 -0
  47. package/dist/local/protocol/ProtocolV35.js.map +1 -0
  48. package/dist/platform.js +284 -67
  49. package/dist/platform.js.map +1 -1
  50. package/dist/shared/AccessoryFactory.js +276 -0
  51. package/dist/shared/AccessoryFactory.js.map +1 -0
  52. package/dist/{accessory → shared/accessories}/AccessoryFactory.js +2 -2
  53. package/dist/shared/accessories/AccessoryFactory.js.map +1 -0
  54. package/dist/shared/accessories/AirConditionerAccessory.js.map +1 -0
  55. package/dist/{accessory → shared/accessories}/AirPurifierAccessory.js +1 -1
  56. package/dist/shared/accessories/AirPurifierAccessory.js.map +1 -0
  57. package/dist/shared/accessories/AirQualitySensorAccessory.js.map +1 -0
  58. package/dist/{accessory → shared/accessories}/BaseAccessory.js +139 -35
  59. package/dist/shared/accessories/BaseAccessory.js.map +1 -0
  60. package/dist/shared/accessories/BlindsAccessory.js +199 -0
  61. package/dist/shared/accessories/BlindsAccessory.js.map +1 -0
  62. package/dist/{accessory → shared/accessories}/CameraAccessory.js +7 -3
  63. package/dist/shared/accessories/CameraAccessory.js.map +1 -0
  64. package/dist/shared/accessories/CarbonDioxideSensorAccessory.js.map +1 -0
  65. package/dist/shared/accessories/CarbonMonoxideSensorAccessory.js.map +1 -0
  66. package/dist/shared/accessories/ContactSensorAccessory.js.map +1 -0
  67. package/dist/shared/accessories/DehumidifierAccessory.js.map +1 -0
  68. package/dist/shared/accessories/DiffuserAccessory.js.map +1 -0
  69. package/dist/shared/accessories/DimmerAccessory.js.map +1 -0
  70. package/dist/{accessory → shared/accessories}/DoorbellAccessory.js +3 -1
  71. package/dist/shared/accessories/DoorbellAccessory.js.map +1 -0
  72. package/dist/{accessory → shared/accessories}/ExtractionHoodAccessory.js +4 -2
  73. package/dist/shared/accessories/ExtractionHoodAccessory.js.map +1 -0
  74. package/dist/{accessory → shared/accessories}/FanAccessory.js +10 -4
  75. package/dist/shared/accessories/FanAccessory.js.map +1 -0
  76. package/dist/shared/accessories/GarageDoorAccessory.js.map +1 -0
  77. package/dist/shared/accessories/HeaterAccessory.js.map +1 -0
  78. package/dist/shared/accessories/HeaterAccessory_old.js.map +1 -0
  79. package/dist/shared/accessories/HumanPresenceSensorAccessory.js.map +1 -0
  80. package/dist/{accessory → shared/accessories}/HumidifierAccessory.js +3 -1
  81. package/dist/shared/accessories/HumidifierAccessory.js.map +1 -0
  82. package/dist/{accessory → shared/accessories}/IRAirConditionerAccessory.js +2 -2
  83. package/dist/shared/accessories/IRAirConditionerAccessory.js.map +1 -0
  84. package/dist/shared/accessories/IRControlHubAccessory.js.map +1 -0
  85. package/dist/shared/accessories/IRControlHubSubAccessory.js.map +1 -0
  86. package/dist/shared/accessories/IRGenericAccessory.js.map +1 -0
  87. package/dist/shared/accessories/LeakSensorAccessory.js.map +1 -0
  88. package/dist/shared/accessories/LightAccessory.js.map +1 -0
  89. package/dist/shared/accessories/LightSensorAccessory.js.map +1 -0
  90. package/dist/shared/accessories/LocationWeatherAccessory.js.map +1 -0
  91. package/dist/shared/accessories/LockAccessory.js.map +1 -0
  92. package/dist/shared/accessories/MotionSensorAccessory.js.map +1 -0
  93. package/dist/shared/accessories/OutletAccessory.js.map +1 -0
  94. package/dist/shared/accessories/PetFeederAccessory.js.map +1 -0
  95. package/dist/shared/accessories/SceneAccessory.js.map +1 -0
  96. package/dist/{accessory → shared/accessories}/SceneSwitchAccessory.js +1 -1
  97. package/dist/shared/accessories/SceneSwitchAccessory.js.map +1 -0
  98. package/dist/shared/accessories/SecuritySystemAccessory.js.map +1 -0
  99. package/dist/shared/accessories/SmokeSensorAccessory.js.map +1 -0
  100. package/dist/shared/accessories/SwitchAccessory.js +148 -0
  101. package/dist/shared/accessories/SwitchAccessory.js.map +1 -0
  102. package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js.map +1 -0
  103. package/dist/shared/accessories/ThermostatAccessory.js.map +1 -0
  104. package/dist/shared/accessories/TowerRackAccessory.js.map +1 -0
  105. package/dist/{accessory → shared/accessories}/ValveAccessory.js +1 -1
  106. package/dist/shared/accessories/ValveAccessory.js.map +1 -0
  107. package/dist/{accessory → shared/accessories}/VibrationSensorAccessory.js +7 -3
  108. package/dist/shared/accessories/VibrationSensorAccessory.js.map +1 -0
  109. package/dist/shared/accessories/WeatherStationAccessory.js.map +1 -0
  110. package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js.map +1 -0
  111. package/dist/shared/accessories/WhiteNoiseLightAccessory.js.map +1 -0
  112. package/dist/shared/accessories/WindowAccessory.js.map +1 -0
  113. package/dist/shared/accessories/WindowCoveringAccessory.js.map +1 -0
  114. package/dist/shared/accessories/WirelessSwitchAccessory.js.map +1 -0
  115. package/dist/shared/accessories/characteristic/Active.js.map +1 -0
  116. package/dist/{accessory → shared/accessories}/characteristic/AirQuality.js +1 -1
  117. package/dist/shared/accessories/characteristic/AirQuality.js.map +1 -0
  118. package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js.map +1 -0
  119. package/dist/shared/accessories/characteristic/CurrentTemperature.js.map +1 -0
  120. package/dist/shared/accessories/characteristic/CurrentWeather.js.map +1 -0
  121. package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js.map +1 -0
  122. package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js.map +1 -0
  123. package/dist/shared/accessories/characteristic/EnergyUsage.js.map +1 -0
  124. package/dist/shared/accessories/characteristic/Light.js.map +1 -0
  125. package/dist/shared/accessories/characteristic/LightSensor.js.map +1 -0
  126. package/dist/shared/accessories/characteristic/LockPhysicalControls.js.map +1 -0
  127. package/dist/{accessory → shared/accessories}/characteristic/MotionDetected.js +1 -1
  128. package/dist/shared/accessories/characteristic/MotionDetected.js.map +1 -0
  129. package/dist/shared/accessories/characteristic/Name.js.map +1 -0
  130. package/dist/shared/accessories/characteristic/OccupancyDetected.js.map +1 -0
  131. package/dist/{accessory → shared/accessories}/characteristic/On.js +1 -1
  132. package/dist/shared/accessories/characteristic/On.js.map +1 -0
  133. package/dist/shared/accessories/characteristic/OutletInUse.js.map +1 -0
  134. package/dist/{accessory → shared/accessories}/characteristic/ProgrammableSwitchEvent.js +2 -2
  135. package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js.map +1 -0
  136. package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js.map +1 -0
  137. package/dist/shared/accessories/characteristic/RotationSpeed.js.map +1 -0
  138. package/dist/shared/accessories/characteristic/SecuritySystemState.js.map +1 -0
  139. package/dist/shared/accessories/characteristic/SwingMode.js.map +1 -0
  140. package/dist/shared/accessories/characteristic/TargetTemperature.js.map +1 -0
  141. package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js.map +1 -0
  142. package/dist/shared/util/ConfigHash.js +79 -0
  143. package/dist/shared/util/ConfigHash.js.map +1 -0
  144. package/dist/shared/util/FfmpegStreamingProcess.js.map +1 -0
  145. package/dist/shared/util/InfraredTool.js.map +1 -0
  146. package/dist/shared/util/Logger.js.map +1 -0
  147. package/dist/shared/util/TuyaRecordingDelegate.js.map +1 -0
  148. package/dist/shared/util/TuyaStreamDelegate.js.map +1 -0
  149. package/dist/shared/util/color.js.map +1 -0
  150. package/dist/shared/util/util.js +135 -0
  151. package/dist/shared/util/util.js.map +1 -0
  152. package/eslint.config.mjs +52 -0
  153. package/jest.config.js +6 -0
  154. package/package.json +23 -28
  155. package/test/__mocks__/@homebridge-camera-utils.js +10 -0
  156. package/test/__mocks__/color-convert.js +24 -0
  157. package/test/__mocks__/kelvin-to-rgb.js +21 -0
  158. package/test/accessoryFactory.test.ts +522 -0
  159. package/test/config.test.ts +430 -0
  160. package/test/custom.test.ts +4 -4
  161. package/test/errorHandling.test.ts +570 -0
  162. package/test/errorHandlingCoverage.test.ts +410 -0
  163. package/test/home.test.ts +4 -4
  164. package/test/localDevice.test.ts +616 -0
  165. package/test/localDeviceManager.behavior.test.ts +185 -0
  166. package/test/localDeviceManager.test.ts +327 -0
  167. package/test/mqttMessages.test.ts +495 -0
  168. package/test/platform.test.ts +1310 -0
  169. package/test/platform.validation.local.test.ts +250 -0
  170. package/test/protocol.test.ts +446 -0
  171. package/test/protocolUtilities.test.ts +351 -0
  172. package/test/sanitizeName.test.ts +2 -1
  173. package/test/tuyaCustomDeviceManager.test.ts +469 -0
  174. package/test/tuyaDevice.test.ts +425 -0
  175. package/test/tuyaDeviceManager.test.ts +813 -0
  176. package/test/tuyaDiscovery.test.ts +439 -0
  177. package/test/tuyaHomeDeviceManager.test.ts +540 -0
  178. package/test/tuyaOpenAPI.test.ts +404 -0
  179. package/test/tuyaOpenMQ.test.ts +444 -0
  180. package/test/util.test.ts +318 -0
  181. package/test/util.ts +4 -3
  182. package/test/zigbee/ChildPayloadUtility.test.ts +199 -0
  183. package/test/zigbee/CloudLocalReconciliation.test.ts +284 -0
  184. package/test/zigbee/DynamicChildDiscovery.test.ts +333 -0
  185. package/test/zigbee/LocalDevice.zigbee.test.ts +274 -0
  186. package/test/zigbee/LocalDeviceManager.cloud-discovery.test.ts +163 -0
  187. package/test/zigbee/LocalDeviceManager.zigbee.reconnect.test.ts +200 -0
  188. package/test/zigbee/LocalDeviceManager.zigbee.test.ts +344 -0
  189. package/test/zigbee/ZigbeeEventDetection.test.ts +418 -0
  190. package/test/zigbee/ZigbeeGatewayDetection.test.ts +222 -0
  191. package/tuya-local-configs/dev-1.hash +1 -0
  192. package/tuya-local-configs/device1.hash +1 -0
  193. package/wiki/Get-Local-Keys-for-Your-Devices.md +85 -0
  194. package/dist/accessory/AccessoryFactory.js.map +0 -1
  195. package/dist/accessory/AirConditionerAccessory.js.map +0 -1
  196. package/dist/accessory/AirPurifierAccessory.js.map +0 -1
  197. package/dist/accessory/AirQualitySensorAccessory.js.map +0 -1
  198. package/dist/accessory/BaseAccessory.js.map +0 -1
  199. package/dist/accessory/CameraAccessory.js.map +0 -1
  200. package/dist/accessory/CarbonDioxideSensorAccessory.js.map +0 -1
  201. package/dist/accessory/CarbonMonoxideSensorAccessory.js.map +0 -1
  202. package/dist/accessory/ContactSensorAccessory.js.map +0 -1
  203. package/dist/accessory/DehumidifierAccessory.js.map +0 -1
  204. package/dist/accessory/DiffuserAccessory.js.map +0 -1
  205. package/dist/accessory/DimmerAccessory.js.map +0 -1
  206. package/dist/accessory/DoorbellAccessory.js.map +0 -1
  207. package/dist/accessory/ExtractionHoodAccessory.js.map +0 -1
  208. package/dist/accessory/FanAccessory.js.map +0 -1
  209. package/dist/accessory/GarageDoorAccessory.js.map +0 -1
  210. package/dist/accessory/HeaterAccessory.js.map +0 -1
  211. package/dist/accessory/HeaterAccessory_old.js.map +0 -1
  212. package/dist/accessory/HumanPresenceSensorAccessory.js.map +0 -1
  213. package/dist/accessory/HumidifierAccessory.js.map +0 -1
  214. package/dist/accessory/IRAirConditionerAccessory.js.map +0 -1
  215. package/dist/accessory/IRControlHubAccessory.js.map +0 -1
  216. package/dist/accessory/IRControlHubSubAccessory.js.map +0 -1
  217. package/dist/accessory/IRGenericAccessory.js.map +0 -1
  218. package/dist/accessory/LeakSensorAccessory.js.map +0 -1
  219. package/dist/accessory/LightAccessory.js.map +0 -1
  220. package/dist/accessory/LightSensorAccessory.js.map +0 -1
  221. package/dist/accessory/LocationWeatherAccessory.js.map +0 -1
  222. package/dist/accessory/LockAccessory.js.map +0 -1
  223. package/dist/accessory/MotionSensorAccessory.js.map +0 -1
  224. package/dist/accessory/OutletAccessory.js.map +0 -1
  225. package/dist/accessory/PetFeederAccessory.js.map +0 -1
  226. package/dist/accessory/SceneAccessory.js.map +0 -1
  227. package/dist/accessory/SceneSwitchAccessory.js.map +0 -1
  228. package/dist/accessory/SecuritySystemAccessory.js.map +0 -1
  229. package/dist/accessory/SmokeSensorAccessory.js.map +0 -1
  230. package/dist/accessory/SwitchAccessory.js +0 -85
  231. package/dist/accessory/SwitchAccessory.js.map +0 -1
  232. package/dist/accessory/TemperatureHumiditySensorAccessory.js.map +0 -1
  233. package/dist/accessory/ThermostatAccessory.js.map +0 -1
  234. package/dist/accessory/TowerRackAccessory.js.map +0 -1
  235. package/dist/accessory/ValveAccessory.js.map +0 -1
  236. package/dist/accessory/VibrationSensorAccessory.js.map +0 -1
  237. package/dist/accessory/WeatherStationAccessory.js.map +0 -1
  238. package/dist/accessory/WetBulbGlobeTemperatureAccessory.js.map +0 -1
  239. package/dist/accessory/WhiteNoiseLightAccessory.js.map +0 -1
  240. package/dist/accessory/WindowAccessory.js.map +0 -1
  241. package/dist/accessory/WindowCoveringAccessory.js.map +0 -1
  242. package/dist/accessory/WirelessSwitchAccessory.js.map +0 -1
  243. package/dist/accessory/characteristic/Active.js.map +0 -1
  244. package/dist/accessory/characteristic/AirQuality.js.map +0 -1
  245. package/dist/accessory/characteristic/CurrentRelativeHumidity.js.map +0 -1
  246. package/dist/accessory/characteristic/CurrentTemperature.js.map +0 -1
  247. package/dist/accessory/characteristic/CurrentWeather.js.map +0 -1
  248. package/dist/accessory/characteristic/CurrentWeatherByOpenMeteo.js.map +0 -1
  249. package/dist/accessory/characteristic/CurrentWetBulbGlobeTemperature.js.map +0 -1
  250. package/dist/accessory/characteristic/EnergyUsage.js.map +0 -1
  251. package/dist/accessory/characteristic/Light.js.map +0 -1
  252. package/dist/accessory/characteristic/LightSensor.js.map +0 -1
  253. package/dist/accessory/characteristic/LockPhysicalControls.js.map +0 -1
  254. package/dist/accessory/characteristic/MotionDetected.js.map +0 -1
  255. package/dist/accessory/characteristic/Name.js.map +0 -1
  256. package/dist/accessory/characteristic/OccupancyDetected.js.map +0 -1
  257. package/dist/accessory/characteristic/On.js.map +0 -1
  258. package/dist/accessory/characteristic/OutletInUse.js.map +0 -1
  259. package/dist/accessory/characteristic/ProgrammableSwitchEvent.js.map +0 -1
  260. package/dist/accessory/characteristic/RelativeHumidityDehumidifierThreshold.js.map +0 -1
  261. package/dist/accessory/characteristic/RotationSpeed.js.map +0 -1
  262. package/dist/accessory/characteristic/SecuritySystemState.js.map +0 -1
  263. package/dist/accessory/characteristic/SwingMode.js.map +0 -1
  264. package/dist/accessory/characteristic/TargetTemperature.js.map +0 -1
  265. package/dist/accessory/characteristic/TemperatureDisplayUnits.js.map +0 -1
  266. package/dist/core/TuyaOpenAPI.js.map +0 -1
  267. package/dist/core/TuyaOpenMQ.js.map +0 -1
  268. package/dist/device/TuyaCustomDeviceManager.js.map +0 -1
  269. package/dist/device/TuyaDevice.js.map +0 -1
  270. package/dist/device/TuyaDeviceManager.js.map +0 -1
  271. package/dist/device/TuyaHomeDeviceManager.js.map +0 -1
  272. package/dist/util/FfmpegStreamingProcess.js.map +0 -1
  273. package/dist/util/InfraredTool.js.map +0 -1
  274. package/dist/util/Logger.js.map +0 -1
  275. package/dist/util/TuyaRecordingDelegate.js.map +0 -1
  276. package/dist/util/TuyaStreamDelegate.js.map +0 -1
  277. package/dist/util/color.js.map +0 -1
  278. package/dist/util/util.js +0 -63
  279. package/dist/util/util.js.map +0 -1
  280. /package/dist/{device → cloud/device}/TuyaCustomDeviceManager.js +0 -0
  281. /package/dist/{device → cloud/device}/TuyaHomeDeviceManager.js +0 -0
  282. /package/dist/{accessory → shared/accessories}/AirConditionerAccessory.js +0 -0
  283. /package/dist/{accessory → shared/accessories}/AirQualitySensorAccessory.js +0 -0
  284. /package/dist/{accessory → shared/accessories}/CarbonDioxideSensorAccessory.js +0 -0
  285. /package/dist/{accessory → shared/accessories}/CarbonMonoxideSensorAccessory.js +0 -0
  286. /package/dist/{accessory → shared/accessories}/ContactSensorAccessory.js +0 -0
  287. /package/dist/{accessory → shared/accessories}/DehumidifierAccessory.js +0 -0
  288. /package/dist/{accessory → shared/accessories}/DiffuserAccessory.js +0 -0
  289. /package/dist/{accessory → shared/accessories}/DimmerAccessory.js +0 -0
  290. /package/dist/{accessory → shared/accessories}/GarageDoorAccessory.js +0 -0
  291. /package/dist/{accessory → shared/accessories}/HeaterAccessory.js +0 -0
  292. /package/dist/{accessory → shared/accessories}/HeaterAccessory_old.js +0 -0
  293. /package/dist/{accessory → shared/accessories}/HumanPresenceSensorAccessory.js +0 -0
  294. /package/dist/{accessory → shared/accessories}/IRControlHubAccessory.js +0 -0
  295. /package/dist/{accessory → shared/accessories}/IRControlHubSubAccessory.js +0 -0
  296. /package/dist/{accessory → shared/accessories}/IRGenericAccessory.js +0 -0
  297. /package/dist/{accessory → shared/accessories}/LeakSensorAccessory.js +0 -0
  298. /package/dist/{accessory → shared/accessories}/LightAccessory.js +0 -0
  299. /package/dist/{accessory → shared/accessories}/LightSensorAccessory.js +0 -0
  300. /package/dist/{accessory → shared/accessories}/LocationWeatherAccessory.js +0 -0
  301. /package/dist/{accessory → shared/accessories}/LockAccessory.js +0 -0
  302. /package/dist/{accessory → shared/accessories}/MotionSensorAccessory.js +0 -0
  303. /package/dist/{accessory → shared/accessories}/OutletAccessory.js +0 -0
  304. /package/dist/{accessory → shared/accessories}/PetFeederAccessory.js +0 -0
  305. /package/dist/{accessory → shared/accessories}/SceneAccessory.js +0 -0
  306. /package/dist/{accessory → shared/accessories}/SecuritySystemAccessory.js +0 -0
  307. /package/dist/{accessory → shared/accessories}/SmokeSensorAccessory.js +0 -0
  308. /package/dist/{accessory → shared/accessories}/TemperatureHumiditySensorAccessory.js +0 -0
  309. /package/dist/{accessory → shared/accessories}/ThermostatAccessory.js +0 -0
  310. /package/dist/{accessory → shared/accessories}/TowerRackAccessory.js +0 -0
  311. /package/dist/{accessory → shared/accessories}/WeatherStationAccessory.js +0 -0
  312. /package/dist/{accessory → shared/accessories}/WetBulbGlobeTemperatureAccessory.js +0 -0
  313. /package/dist/{accessory → shared/accessories}/WhiteNoiseLightAccessory.js +0 -0
  314. /package/dist/{accessory → shared/accessories}/WindowAccessory.js +0 -0
  315. /package/dist/{accessory → shared/accessories}/WindowCoveringAccessory.js +0 -0
  316. /package/dist/{accessory → shared/accessories}/WirelessSwitchAccessory.js +0 -0
  317. /package/dist/{accessory → shared/accessories}/characteristic/Active.js +0 -0
  318. /package/dist/{accessory → shared/accessories}/characteristic/CurrentRelativeHumidity.js +0 -0
  319. /package/dist/{accessory → shared/accessories}/characteristic/CurrentTemperature.js +0 -0
  320. /package/dist/{accessory → shared/accessories}/characteristic/CurrentWeather.js +0 -0
  321. /package/dist/{accessory → shared/accessories}/characteristic/CurrentWeatherByOpenMeteo.js +0 -0
  322. /package/dist/{accessory → shared/accessories}/characteristic/CurrentWetBulbGlobeTemperature.js +0 -0
  323. /package/dist/{accessory → shared/accessories}/characteristic/EnergyUsage.js +0 -0
  324. /package/dist/{accessory → shared/accessories}/characteristic/Light.js +0 -0
  325. /package/dist/{accessory → shared/accessories}/characteristic/LightSensor.js +0 -0
  326. /package/dist/{accessory → shared/accessories}/characteristic/LockPhysicalControls.js +0 -0
  327. /package/dist/{accessory → shared/accessories}/characteristic/Name.js +0 -0
  328. /package/dist/{accessory → shared/accessories}/characteristic/OccupancyDetected.js +0 -0
  329. /package/dist/{accessory → shared/accessories}/characteristic/OutletInUse.js +0 -0
  330. /package/dist/{accessory → shared/accessories}/characteristic/RelativeHumidityDehumidifierThreshold.js +0 -0
  331. /package/dist/{accessory → shared/accessories}/characteristic/RotationSpeed.js +0 -0
  332. /package/dist/{accessory → shared/accessories}/characteristic/SecuritySystemState.js +0 -0
  333. /package/dist/{accessory → shared/accessories}/characteristic/SwingMode.js +0 -0
  334. /package/dist/{accessory → shared/accessories}/characteristic/TargetTemperature.js +0 -0
  335. /package/dist/{accessory → shared/accessories}/characteristic/TemperatureDisplayUnits.js +0 -0
  336. /package/dist/{util → shared/util}/FfmpegStreamingProcess.js +0 -0
  337. /package/dist/{util → shared/util}/InfraredTool.js +0 -0
  338. /package/dist/{util → shared/util}/Logger.js +0 -0
  339. /package/dist/{util → shared/util}/TuyaRecordingDelegate.js +0 -0
  340. /package/dist/{util → shared/util}/TuyaStreamDelegate.js +0 -0
  341. /package/dist/{util → shared/util}/color.js +0 -0
@@ -0,0 +1,981 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const TuyaDevice_1 = __importStar(require("../cloud/device/TuyaDevice"));
40
+ const TuyaOpenAPI_1 = __importDefault(require("../cloud/api/TuyaOpenAPI"));
41
+ const TuyaDeviceManager_1 = __importDefault(require("../cloud/device/TuyaDeviceManager"));
42
+ const TuyaDiscovery_1 = __importDefault(require("./TuyaDiscovery"));
43
+ const LocalDevice_1 = __importDefault(require("./LocalDevice"));
44
+ const Logger_1 = require("../shared/util/Logger");
45
+ const ConfigHash_1 = require("../shared/util/ConfigHash");
46
+ const ZigbeeGatewayDetection_1 = require("./ZigbeeGatewayDetection");
47
+ const DynamicChildDiscovery_1 = require("./DynamicChildDiscovery");
48
+ /**
49
+ * Default DP-to-code mapping used when the user hasn't supplied one.
50
+ * Keys are Tuya standard instruction codes; values are typical DP numbers.
51
+ * This covers the most common single-switch, dimmer and RGBTW combinations.
52
+ * Users can override via LocalDeviceConfig.dpMapping.
53
+ */
54
+ const DEFAULT_DP_MAP = {
55
+ switch_1: 1,
56
+ switch_2: 2,
57
+ switch_3: 3,
58
+ switch_4: 4,
59
+ bright_value: 2,
60
+ bright_value_v2: 22,
61
+ temp_value: 3,
62
+ temp_value_v2: 23,
63
+ work_mode: 2,
64
+ colour_data: 5,
65
+ colour_data_v2: 24,
66
+ switch_led: 1,
67
+ countdown_1: 9,
68
+ };
69
+ /** Inverse of DEFAULT_DP_MAP: dp number → code */
70
+ function buildDpToCodeMap(dpMapping) {
71
+ const inv = {};
72
+ for (const [code, dp] of Object.entries(dpMapping)) {
73
+ // Only map first code per dp (prefer explicit mappings)
74
+ if (!(dp in inv)) {
75
+ inv[dp] = code;
76
+ }
77
+ }
78
+ return inv;
79
+ }
80
+ /**
81
+ * LocalDeviceManager – extends TuyaDeviceManager so the existing platform code
82
+ * can treat it (and use its `devices` array / events) without any changes.
83
+ *
84
+ * Uses a dummy TuyaOpenAPI so the parent class initialises without connecting
85
+ * to the Tuya cloud. The MQTT client created by the parent is stopped
86
+ * immediately in the constructor.
87
+ *
88
+ * Local devices are represented as TuyaDevice objects with synthetic schemas
89
+ * derived from the user-supplied (or default) dpMapping. Status is tracked as
90
+ * `{code, value}` pairs, making them compatible with all existing accessory
91
+ * handlers.
92
+ */
93
+ class LocalDeviceManager extends TuyaDeviceManager_1.default {
94
+ // ──────────────────────────────────────────────────────────────────────────
95
+ constructor(localConfig, log, persistPath) {
96
+ // Create a minimal dummy API so the parent initialises without connecting
97
+ const dummyLog = new Logger_1.PrefixLogger(log, 'LocalDummy', false);
98
+ const dummyApi = new TuyaOpenAPI_1.default('', '', '', dummyLog);
99
+ super(dummyApi, false);
100
+ this.localDevices = [];
101
+ this.localConnections = new Map();
102
+ this.discoveryTimeout = null;
103
+ this.rediscoveryTimeout = null;
104
+ this.isDiscoveryActive = false;
105
+ this.discoveredInCurrentPhase = new Set();
106
+ // Maps deviceId → effective dpMapping (code → dp)
107
+ this.dpMaps = new Map();
108
+ // Maps deviceId → reverse dpMapping (dp → code)
109
+ this.reverseDpMaps = new Map();
110
+ // Maps discovered gwId → IP (kept even after TuyaDevice created)
111
+ this.discoveredIPs = new Map();
112
+ // Maps discovered gwId → version
113
+ this.discoveredVersions = new Map();
114
+ // Pending local response watches to cancel superseded commands
115
+ this.pendingLocalResponseWatchers = new Map();
116
+ // Tracks which devices had config changes
117
+ this.configChanged = new Map();
118
+ // ── Zigbee gateway/child support ──────────────────────────────────────────
119
+ /** Parent gateway ID → GatewayRelationship (derived from config at init). */
120
+ this.gatewayRelationships = new Map();
121
+ /** Parent gateway device ID → its active LocalDevice connection. */
122
+ this.gatewayConnections = new Map();
123
+ // Stop the MQ that the parent started
124
+ this.mq.stop();
125
+ this.parentLog = log;
126
+ this.log = new Logger_1.PrefixLogger(log, 'LocalDeviceManager', false);
127
+ this.config = localConfig;
128
+ this.discovery = new TuyaDiscovery_1.default(log);
129
+ this.configHash = new ConfigHash_1.ConfigHash(persistPath || '', 'tuya-local-configs');
130
+ }
131
+ /**
132
+ * Initialise discovery and register manually configured devices.
133
+ * Returns immediately; discovery events fire asynchronously.
134
+ */
135
+ async initLocalDevices() {
136
+ // ── Zigbee: detect parent-child relationships before registering devices ──
137
+ if (this.config.devices && this.config.devices.length > 0) {
138
+ try {
139
+ this.gatewayRelationships = ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.detectFromDevices(this.config.devices);
140
+ if (this.gatewayRelationships.size > 0) {
141
+ for (const [parentId, rel] of this.gatewayRelationships) {
142
+ this.log.info(`Zigbee gateway detected: ${parentId} has ${rel.children.length} sub-device(s): ` +
143
+ rel.children.map(c => `${c.name} (CID=${c.cid})`).join(', '));
144
+ }
145
+ }
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ this.log.error(`Zigbee configuration error: ${msg}`);
150
+ // Continue without Zigbee – non-fatal for non-Zigbee devices
151
+ }
152
+ }
153
+ // Register manual device entries first (non-child devices only at this stage)
154
+ if (this.config.devices) {
155
+ for (const cfg of this.config.devices) {
156
+ // Skip Zigbee children here – they are set up after their parent connects
157
+ if (ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.isChild(cfg)) {
158
+ continue;
159
+ }
160
+ this._registerDeviceConfig(cfg);
161
+ }
162
+ // Validate devices have keys if auto-discovery is disabled
163
+ if (this.config.autoDiscoverDevices === false) {
164
+ for (const cfg of this.config.devices) {
165
+ if (!cfg.tuyaKey && !ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.isChild(cfg)) {
166
+ const deviceName = cfg.name || cfg.tuyaDeviceId;
167
+ this.log.warn(`Local device "${deviceName}" (${cfg.tuyaDeviceId}) is missing tuyaKey ` +
168
+ 'and auto-discovery is disabled. This device will not be usable.');
169
+ }
170
+ }
171
+ }
172
+ }
173
+ // Start UDP discovery unless explicitly disabled
174
+ if (this.config.autoDiscoverDevices !== false) {
175
+ this.discovery.on('discover', (result) => {
176
+ this._onDiscovered(result);
177
+ });
178
+ this._startDiscoveryPhase();
179
+ // Schedule periodic rediscovery to handle devices that go offline and return
180
+ // (e.g., after WiFi reset or IP change)
181
+ const rediscoverIntervalSec = this.config.rediscoverInterval ?? 900; // Default 15 minutes
182
+ if (rediscoverIntervalSec > 0) {
183
+ this.log.info(`Scheduling periodic device rediscovery every ${rediscoverIntervalSec} seconds`);
184
+ this._schedulePeriodicRediscovery(rediscoverIntervalSec);
185
+ }
186
+ }
187
+ }
188
+ /** Stop discovery and disconnect all local TCP connections. */
189
+ stopLocalDevices() {
190
+ if (this.discoveryTimeout) {
191
+ clearTimeout(this.discoveryTimeout);
192
+ this.discoveryTimeout = null;
193
+ }
194
+ if (this.rediscoveryTimeout) {
195
+ clearTimeout(this.rediscoveryTimeout);
196
+ this.rediscoveryTimeout = null;
197
+ }
198
+ this.discovery.stop();
199
+ this.isDiscoveryActive = false;
200
+ for (const ld of this.localConnections.values()) {
201
+ ld.disconnect();
202
+ }
203
+ this.localConnections.clear();
204
+ // Gateway connections are separate from localConnections
205
+ for (const gw of this.gatewayConnections.values()) {
206
+ gw.disconnect();
207
+ }
208
+ this.gatewayConnections.clear();
209
+ }
210
+ /** Returns ALL devices (both local shadow entries from this manager). */
211
+ getDevice(deviceID) {
212
+ return this.localDevices.find(d => d.id === deviceID);
213
+ }
214
+ /**
215
+ * Send commands to a local device.
216
+ * `commands` is the same [{code, value}] format used by the cloud API so
217
+ * all existing accessory handlers work without modification.
218
+ */
219
+ async sendCommands(deviceID, commands) {
220
+ const dpMap = this.dpMaps.get(deviceID);
221
+ if (!dpMap) {
222
+ this.log.warn(`No dpMapping for local device ${deviceID}`);
223
+ return;
224
+ }
225
+ const dps = {};
226
+ for (const { code, value } of commands) {
227
+ const dp = dpMap[code];
228
+ if (dp === undefined) {
229
+ this.log.warn(`Unknown code "${code}" for device ${deviceID}`);
230
+ continue;
231
+ }
232
+ dps[String(dp)] = value;
233
+ }
234
+ if (Object.keys(dps).length === 0) {
235
+ return;
236
+ }
237
+ // Log the command for user visibility
238
+ const device = this.localDevices.find(d => d.id === deviceID);
239
+ const deviceName = device?.name || deviceID;
240
+ const commandStr = commands.map(c => `${c.code}=${c.value}`).join(', ');
241
+ this.log.info(`[${deviceName}] Sending command (local): ${commandStr}`);
242
+ // ── Zigbee child path ────────────────────────────────────────────────
243
+ const childConn = this.localConnections.get(deviceID);
244
+ if (childConn?.parentDevice) {
245
+ // Already set up as a Zigbee child – LocalDevice.update() delegates to parent
246
+ childConn.update(dps);
247
+ return true;
248
+ }
249
+ // ─────────────────────────────────────────────────────────────────────
250
+ let conn = this.localConnections.get(deviceID);
251
+ if (!conn) {
252
+ conn = this._createConnection(deviceID);
253
+ if (!conn) {
254
+ return;
255
+ }
256
+ }
257
+ const expectedDps = new Set(Object.keys(dps));
258
+ this._cancelSupersededLocalResponseWatchers(deviceID, expectedDps);
259
+ const shouldWaitForResponse = conn.connected === true;
260
+ let timer = null;
261
+ let responsePromise;
262
+ let watcher = null;
263
+ if (shouldWaitForResponse) {
264
+ responsePromise = new Promise((resolve, reject) => {
265
+ const cleanup = () => {
266
+ if (timer) {
267
+ clearTimeout(timer);
268
+ timer = null;
269
+ }
270
+ conn?.removeListener('change', onChange);
271
+ conn?.removeListener('error', onError);
272
+ if (!watcher) {
273
+ return;
274
+ }
275
+ const watchers = this.pendingLocalResponseWatchers.get(deviceID) ?? [];
276
+ const remaining = watchers.filter(w => w !== watcher);
277
+ if (remaining.length > 0) {
278
+ this.pendingLocalResponseWatchers.set(deviceID, remaining);
279
+ }
280
+ else {
281
+ this.pendingLocalResponseWatchers.delete(deviceID);
282
+ }
283
+ };
284
+ const onChange = (changes) => {
285
+ for (const dp of Object.keys(changes)) {
286
+ if (expectedDps.has(dp)) {
287
+ cleanup();
288
+ resolve(true);
289
+ return;
290
+ }
291
+ }
292
+ };
293
+ const onError = (err) => {
294
+ cleanup();
295
+ reject(err);
296
+ };
297
+ watcher = { expectedDps, cleanup, resolve, reject };
298
+ const watchers = this.pendingLocalResponseWatchers.get(deviceID) ?? [];
299
+ watchers.push(watcher);
300
+ this.pendingLocalResponseWatchers.set(deviceID, watchers);
301
+ timer = setTimeout(() => {
302
+ cleanup();
303
+ reject(new Error('Local command response timeout'));
304
+ }, 10 * 1000);
305
+ conn?.on('change', onChange);
306
+ conn?.on('error', onError);
307
+ });
308
+ }
309
+ conn.update(dps);
310
+ if (!responsePromise) {
311
+ this.log.debug(`Local device ${deviceName} is not yet connected; skipping response wait.`);
312
+ return true;
313
+ }
314
+ try {
315
+ await responsePromise;
316
+ return true;
317
+ }
318
+ catch (error) {
319
+ this.log.warn(`Local command timeout or error for ${deviceName}: ${error instanceof Error ? error.message : error}`);
320
+ throw error;
321
+ }
322
+ }
323
+ _cancelSupersededLocalResponseWatchers(deviceID, expectedDps) {
324
+ const watchers = this.pendingLocalResponseWatchers.get(deviceID);
325
+ if (!watchers || watchers.length === 0) {
326
+ return;
327
+ }
328
+ const remaining = [];
329
+ for (const watcher of watchers) {
330
+ let overlap = false;
331
+ for (const dp of watcher.expectedDps) {
332
+ if (expectedDps.has(dp)) {
333
+ overlap = true;
334
+ break;
335
+ }
336
+ }
337
+ if (overlap) {
338
+ watcher.cleanup();
339
+ watcher.resolve(true);
340
+ }
341
+ else {
342
+ remaining.push(watcher);
343
+ }
344
+ }
345
+ if (remaining.length > 0) {
346
+ this.pendingLocalResponseWatchers.set(deviceID, remaining);
347
+ }
348
+ else {
349
+ this.pendingLocalResponseWatchers.delete(deviceID);
350
+ }
351
+ }
352
+ handleCloudStatusUpdate(deviceID, statusUpdates) {
353
+ const device = this.localDevices.find(d => d.id === deviceID);
354
+ if (!device) {
355
+ return;
356
+ }
357
+ const dpMap = this.dpMaps.get(deviceID) ?? {};
358
+ const statusDps = new Set();
359
+ for (const status of statusUpdates) {
360
+ const dp = dpMap[status.code];
361
+ statusDps.add(dp !== undefined ? String(dp) : String(status.code));
362
+ const existing = device.status.find(s => s.code === status.code);
363
+ if (existing) {
364
+ existing.value = status.value;
365
+ }
366
+ else {
367
+ device.status.push({ code: status.code, value: status.value });
368
+ }
369
+ }
370
+ const watchers = this.pendingLocalResponseWatchers.get(deviceID);
371
+ if (!watchers || watchers.length === 0) {
372
+ return;
373
+ }
374
+ const remaining = [];
375
+ for (const watcher of watchers) {
376
+ let matched = false;
377
+ for (const dp of watcher.expectedDps) {
378
+ if (statusDps.has(dp)) {
379
+ matched = true;
380
+ break;
381
+ }
382
+ }
383
+ if (matched) {
384
+ watcher.cleanup();
385
+ watcher.resolve(true);
386
+ }
387
+ else {
388
+ remaining.push(watcher);
389
+ }
390
+ }
391
+ if (remaining.length > 0) {
392
+ this.pendingLocalResponseWatchers.set(deviceID, remaining);
393
+ }
394
+ else {
395
+ this.pendingLocalResponseWatchers.delete(deviceID);
396
+ }
397
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, device, statusUpdates);
398
+ }
399
+ // ── Private helpers ────────────────────────────────────────────────────────
400
+ /**
401
+ * Start a discovery phase (e.g. initial startup or periodic rediscovery).
402
+ * Runs discovery for discoverTimeout seconds, then stops.
403
+ */
404
+ _startDiscoveryPhase() {
405
+ if (this.isDiscoveryActive) {
406
+ return; // Discovery already running
407
+ }
408
+ this.isDiscoveryActive = true;
409
+ this.discoveredInCurrentPhase.clear();
410
+ this.log.info('Starting discovery phase');
411
+ this.discovery.start();
412
+ // Stop after configurable timeout (config is in seconds, default 60s)
413
+ // Convert to milliseconds
414
+ const timeoutMs = (this.config.discoverTimeout ?? 60) * 1000;
415
+ if (this.discoveryTimeout) {
416
+ clearTimeout(this.discoveryTimeout);
417
+ }
418
+ this.discoveryTimeout = setTimeout(() => {
419
+ this.isDiscoveryActive = false;
420
+ this.discovery.stop();
421
+ // Report discovery results for clarity
422
+ if (this.discoveredInCurrentPhase.size > 0) {
423
+ this.log.info(`Ended discovery phase – rediscovered ${this.discoveredInCurrentPhase.size} device(s)`);
424
+ }
425
+ else {
426
+ // No devices found; they may be using cached IPs from previous discovery
427
+ const knownDevices = this.localDevices.filter(d => d.localKey);
428
+ if (knownDevices.length > 0) {
429
+ this.log.info(`Ended discovery phase – no new devices found (${knownDevices.length} configured device(s) using cached IP)`);
430
+ }
431
+ else {
432
+ this.log.info('Ended discovery phase – no devices found');
433
+ }
434
+ }
435
+ this.discoveryTimeout = null;
436
+ }, timeoutMs);
437
+ this.discoveryTimeout.unref();
438
+ }
439
+ /**
440
+ * Schedule periodic rediscovery to detect devices that go offline and return.
441
+ * This helps handle WiFi resets, IP changes, and network reconnections.
442
+ */
443
+ _schedulePeriodicRediscovery(intervalSec) {
444
+ if (this.rediscoveryTimeout) {
445
+ clearTimeout(this.rediscoveryTimeout);
446
+ }
447
+ const intervalMs = intervalSec * 1000;
448
+ this.rediscoveryTimeout = setTimeout(() => {
449
+ this.log.debug('Triggering periodic device rediscovery');
450
+ this._startDiscoveryPhase();
451
+ // Reschedule for the next cycle
452
+ this._schedulePeriodicRediscovery(intervalSec);
453
+ }, intervalMs);
454
+ this.rediscoveryTimeout.unref();
455
+ }
456
+ _registerDeviceConfig(cfg) {
457
+ if (!cfg.tuyaDeviceId) {
458
+ const name = cfg.name ?? 'unknown';
459
+ this.log.warn(`Skipping invalid local config entry for ${name}: missing tuyaDeviceId`);
460
+ return;
461
+ }
462
+ const effectiveMap = Object.assign({}, DEFAULT_DP_MAP, cfg.dpMapping ?? {});
463
+ this.dpMaps.set(cfg.tuyaDeviceId, effectiveMap);
464
+ this.reverseDpMaps.set(cfg.tuyaDeviceId, buildDpToCodeMap(effectiveMap));
465
+ const existing = this.localDevices.find(d => d.id === cfg.tuyaDeviceId);
466
+ // Check if config has changed since last run
467
+ // Hash the config fields that affect device behavior
468
+ const configToHash = {
469
+ tuyaDeviceId: cfg.tuyaDeviceId,
470
+ name: cfg.name,
471
+ tuyaKey: cfg.tuyaKey,
472
+ dpMapping: cfg.dpMapping,
473
+ switchCount: cfg.switchCount,
474
+ outletCount: cfg.outletCount,
475
+ protocolVersion: cfg.protocolVersion,
476
+ category: cfg.category,
477
+ };
478
+ const { changed: configChanged } = this.configHash.hasConfigChanged(cfg.tuyaDeviceId, configToHash);
479
+ this.configChanged.set(cfg.tuyaDeviceId, configChanged);
480
+ if (configChanged && existing) {
481
+ this.log.info(`Device ${cfg.tuyaDeviceId}: config changed, rebuilding schema`);
482
+ }
483
+ if (!existing) {
484
+ const device = this._buildTuyaDevice(cfg, configChanged);
485
+ this.localDevices.push(device);
486
+ this.devices.push(device); // keep parent's devices array in sync
487
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_ADD, device);
488
+ }
489
+ }
490
+ /**
491
+ * Set up auto-detection listener to count switches from device status updates.
492
+ * Called BEFORE conn.connect() to avoid race conditions.
493
+ * Uses time-based accumulation to capture all switches across multiple updates.
494
+ */
495
+ _setupAutoDetectionListener(deviceId, device, conn) {
496
+ const cfg = this.config.devices?.find(d => d.tuyaDeviceId === deviceId);
497
+ if (!cfg || (cfg.switchCount || cfg.outletCount)) {
498
+ return; // Already has explicit count
499
+ }
500
+ this.log.debug(`[AutoDetect] Setting up auto-detection listener for ${deviceId}`);
501
+ const detectedSwitches = new Set();
502
+ let detectionTimer = null;
503
+ const onStatusChange = (changes) => {
504
+ this.log.debug(`[AutoDetect] Status change received for ${deviceId}: ${JSON.stringify(Object.keys(changes))}`);
505
+ // Collect switch numbers from incoming changes
506
+ const dpMap = this.dpMaps.get(deviceId) ?? DEFAULT_DP_MAP;
507
+ const reverseMap = buildDpToCodeMap(dpMap);
508
+ for (const dpStr of Object.keys(changes)) {
509
+ const dpNum = parseInt(dpStr, 10);
510
+ const code = reverseMap[dpNum];
511
+ const switchMatch = code?.match(/^switch_(\d+)$/);
512
+ if (switchMatch) {
513
+ const switchNum = parseInt(switchMatch[1], 10);
514
+ detectedSwitches.add(switchNum);
515
+ this.log.debug(`[AutoDetect] Found switch_${switchNum} (dp=${dpNum}, code=${code})`);
516
+ }
517
+ }
518
+ // Restart the detection timer
519
+ if (detectionTimer) {
520
+ clearTimeout(detectionTimer);
521
+ }
522
+ detectionTimer = setTimeout(() => {
523
+ // After 2 seconds of no new switches, finalize auto-detection
524
+ if (detectedSwitches.size > 0) {
525
+ const detectedCount = Math.max(...Array.from(detectedSwitches));
526
+ const switchList = Array.from(detectedSwitches).sort((a, b) => a - b).join(', ');
527
+ this.log.info(`[AutoDetect] Auto-detected ${detectedCount} switch(es) for "${cfg.name}" (${deviceId}): ` +
528
+ `switches=[${switchList}]`);
529
+ // Update device schema with detected switch count
530
+ device.schema = this._buildSyntheticSchema(this.dpMaps.get(deviceId) ?? DEFAULT_DP_MAP, detectedCount);
531
+ device.switchCount = detectedCount;
532
+ // Auto-detection is now complete
533
+ device.isAutoDetecting = false;
534
+ // Recalculate config hash since the schema has changed
535
+ // This ensures SwitchAccessory knows to rebuild services
536
+ const updatedConfigToHash = {
537
+ tuyaDeviceId: cfg.tuyaDeviceId,
538
+ name: cfg.name,
539
+ tuyaKey: cfg.tuyaKey,
540
+ dpMapping: cfg.dpMapping,
541
+ switchCount: detectedCount,
542
+ outletCount: cfg.outletCount,
543
+ protocolVersion: cfg.protocolVersion,
544
+ category: cfg.category,
545
+ };
546
+ const { changed: schemaChanged } = this.configHash.hasConfigChanged(deviceId, updatedConfigToHash);
547
+ if (schemaChanged) {
548
+ this.log.debug('[AutoDetect] Schema changed after auto-detection, marking config as changed');
549
+ device.configChanged = true;
550
+ this.configChanged.set(deviceId, true);
551
+ }
552
+ // Notify the platform so configureServices() is re-run and switch_2+ services are added.
553
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, device, { schemaUpdated: true });
554
+ }
555
+ else {
556
+ this.log.debug(`[AutoDetect] No switches detected after 2 seconds for ${deviceId}`);
557
+ }
558
+ // Remove listener after detection is complete
559
+ conn.removeListener('change', onStatusChange);
560
+ }, 2000);
561
+ };
562
+ // Listen for status changes (multiple events to capture all switches)
563
+ conn.on('change', onStatusChange);
564
+ this.log.debug(`[AutoDetect] Listener attached to connection for ${deviceId}`);
565
+ }
566
+ _onDiscovered(result) {
567
+ this.discoveredIPs.set(result.id, result.ip);
568
+ this.discoveredVersions.set(result.id, result.version);
569
+ this.discoveredInCurrentPhase.add(result.id);
570
+ // If we already have a TuyaDevice for this ID (from manual config), update its IP
571
+ const existing = this.localDevices.find(d => d.id === result.id);
572
+ if (existing) {
573
+ this.log.info(`Device ${result.id} (v${result.version}) rediscovered @ ${result.ip} ` +
574
+ '– updating connection');
575
+ existing.ip = result.ip;
576
+ // Update protocol version if not user-configured
577
+ const typed = existing;
578
+ if (!typed.cfgVersion) {
579
+ typed.localVersion = result.version;
580
+ }
581
+ // If a connection exists, reconnect with updated IP/version
582
+ const conn = this.localConnections.get(result.id);
583
+ if (conn) {
584
+ conn.disconnect();
585
+ this.localConnections.delete(result.id);
586
+ }
587
+ // Try to establish a connection if the device has a local key
588
+ const localKey = existing.localKey;
589
+ if (localKey) {
590
+ this._createConnection(result.id);
591
+ }
592
+ return;
593
+ }
594
+ // New device discovered via UDP – create a placeholder TuyaDevice
595
+ // (it won't be controllable until the user provides a tuyaKey via config)
596
+ const device = this._buildDiscoveredDevice(result);
597
+ this.localDevices.push(device);
598
+ this.devices.push(device);
599
+ const productInfo = result.productKey ? ` productKey=${result.productKey}` : '';
600
+ this.log.info(`Local device discovered: ${result.id} @ ${result.ip} (v${result.version}${productInfo}) ` +
601
+ '– add to config with tuyaKey to control');
602
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_ADD, device);
603
+ }
604
+ _buildTuyaDevice(cfg, configChanged = false) {
605
+ const device = new TuyaDevice_1.default({});
606
+ const deviceId = cfg.tuyaDeviceId ?? '';
607
+ device.id = deviceId;
608
+ device.uuid = deviceId;
609
+ device.name = cfg.name ?? `LocalDevice-${deviceId ? deviceId.slice(0, 8) : 'unknown'}`;
610
+ device.category = cfg.category ?? 'unknown';
611
+ device.ip = cfg.ip ?? (deviceId ? this.discoveredIPs.get(deviceId) ?? '' : '');
612
+ device.online = false;
613
+ device.product_id = '';
614
+ device.product_name = 'Local Device';
615
+ device.model = '';
616
+ device.icon = '';
617
+ device.owner_id = '';
618
+ device.lat = '';
619
+ device.lon = '';
620
+ device.time_zone = '';
621
+ device.create_time = 0;
622
+ device.active_time = 0;
623
+ device.update_time = 0;
624
+ device.status = [];
625
+ const switchCount = cfg.switchCount ?? cfg.outletCount ?? 1;
626
+ const autoDetectNote = !cfg.switchCount && !cfg.outletCount && cfg.tuyaKey ? ' (auto-detecting on connect)' : '';
627
+ const isAutoDetecting = !!(cfg.tuyaKey && !cfg.switchCount && !cfg.outletCount);
628
+ this.log.info(`Building synthetic schema for ${cfg.name} with switchCount=${switchCount}${autoDetectNote}`);
629
+ device.schema = this._buildSyntheticSchema(Object.assign({}, DEFAULT_DP_MAP, cfg.dpMapping ?? {}), switchCount);
630
+ this.log.info(`Device "${cfg.name}" schema includes ${device.schema.length} codes: ${device.schema.map(s => s.code).join(', ')}`);
631
+ // Store local-specific metadata directly on the device object for later use
632
+ device.localKey = cfg.tuyaKey;
633
+ device.localIp = cfg.ip;
634
+ device.localVersion =
635
+ cfg.protocolVersion ?? this.discoveredVersions.get(cfg.tuyaDeviceId);
636
+ // Track whether version was user-configured (prevents overwrite on re-discovery)
637
+ device.cfgVersion = cfg.protocolVersion;
638
+ device.isLocal = true;
639
+ device.switchCount = switchCount;
640
+ device.isAutoDetecting = isAutoDetecting;
641
+ // Track whether config changed (used by SwitchAccessory to decide whether to remove cached extras)
642
+ device.configChanged = configChanged;
643
+ return device;
644
+ }
645
+ _buildDiscoveredDevice(result) {
646
+ const device = new TuyaDevice_1.default({});
647
+ device.id = result.id;
648
+ device.uuid = result.id;
649
+ device.name = `Local-${result.id.slice(0, 8)}`;
650
+ device.category = 'unknown';
651
+ device.ip = result.ip;
652
+ device.online = false;
653
+ device.product_id = result.productKey ?? '';
654
+ device.product_name = 'Local Device';
655
+ device.model = '';
656
+ device.icon = '';
657
+ device.owner_id = '';
658
+ device.lat = '';
659
+ device.lon = '';
660
+ device.time_zone = '';
661
+ device.create_time = 0;
662
+ device.active_time = 0;
663
+ device.update_time = 0;
664
+ device.status = [];
665
+ device.schema = [];
666
+ device.isLocal = true;
667
+ device.localVersion = result.version;
668
+ return device;
669
+ }
670
+ _buildSyntheticSchema(dpMapping, switchCount = 1) {
671
+ // If switchCount is limited, filter switch-related entries to only the needed ones
672
+ let filteredMapping = dpMapping;
673
+ if (switchCount > 0) {
674
+ // Only include switch_1 through switch_N where N = switchCount
675
+ // Also exclude other switch variants (like switch_led, switch_inching) if limited
676
+ filteredMapping = {};
677
+ const switchCodes = [];
678
+ for (const [code, dp] of Object.entries(dpMapping)) {
679
+ // Keep non-switch DPs (brightness, temp, color, etc.)
680
+ if (!code.startsWith('switch')) {
681
+ filteredMapping[code] = dp;
682
+ }
683
+ // For switches, only include switch_1..switch_N
684
+ const switchMatch = code.match(/^switch_(\d+)$/);
685
+ if (switchMatch) {
686
+ const switchNum = parseInt(switchMatch[1], 10);
687
+ if (switchNum <= switchCount) {
688
+ filteredMapping[code] = dp;
689
+ switchCodes.push(code);
690
+ }
691
+ }
692
+ }
693
+ if (switchCodes.length < Object.keys(dpMapping).filter(k => k.startsWith('switch')).length) {
694
+ this.log.debug(`Filtered switches: kept [${switchCodes.join(', ')}], excluded others`);
695
+ }
696
+ }
697
+ return Object.keys(filteredMapping).map(code => ({
698
+ code,
699
+ mode: TuyaDevice_1.TuyaDeviceSchemaMode.READ_WRITE,
700
+ type: TuyaDevice_1.TuyaDeviceSchemaType.Boolean, // best-effort default; handlers may override
701
+ property: {},
702
+ }));
703
+ }
704
+ _createConnection(deviceID) {
705
+ const device = this.localDevices.find(d => d.id === deviceID);
706
+ if (!device) {
707
+ return undefined;
708
+ }
709
+ const localKey = device.localKey;
710
+ if (!localKey) {
711
+ this.log.warn(`No tuyaKey configured for local device ${deviceID} — cannot connect.`);
712
+ return undefined;
713
+ }
714
+ const ip = device.ip || device.localIp;
715
+ if (!ip) {
716
+ this.log.warn(`No IP known for local device ${deviceID} — cannot connect.`);
717
+ return undefined;
718
+ }
719
+ const version = device.localVersion
720
+ ?? this.discoveredVersions.get(deviceID)
721
+ ?? '3.3';
722
+ const conn = new LocalDevice_1.default({
723
+ id: deviceID,
724
+ key: Buffer.from(localKey, 'utf8'),
725
+ ip,
726
+ version,
727
+ name: device.name,
728
+ }, this.parentLog);
729
+ conn.on('connect', () => {
730
+ device.online = true;
731
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, device, { online: true });
732
+ // ── Zigbee: when a gateway connects, set up its children ──
733
+ if (this.gatewayRelationships.has(deviceID)) {
734
+ this.gatewayConnections.set(deviceID, conn);
735
+ this._setupZigbeeChildren(deviceID, conn);
736
+ }
737
+ });
738
+ conn.on('disconnect', () => {
739
+ device.online = false;
740
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, device, { online: false });
741
+ });
742
+ conn.on('change', (changes) => {
743
+ const reverseMap = this.reverseDpMaps.get(deviceID) ?? {};
744
+ const statusUpdates = [];
745
+ for (const [dp, val] of Object.entries(changes)) {
746
+ const code = reverseMap[parseInt(dp, 10)] ?? dp;
747
+ // Update device.status array
748
+ const existing = device.status.find(s => s.code === code);
749
+ if (existing) {
750
+ existing.value = val;
751
+ }
752
+ else {
753
+ device.status.push({ code, value: val });
754
+ }
755
+ statusUpdates.push({ code, value: val });
756
+ }
757
+ if (statusUpdates.length > 0) {
758
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, device, statusUpdates);
759
+ }
760
+ });
761
+ conn.on('error', (err) => {
762
+ this.log.warn(`Error for local device ${deviceID}: ${err.message}`);
763
+ });
764
+ // Set up auto-detection listener BEFORE connecting to avoid race conditions
765
+ this._setupAutoDetectionListener(deviceID, device, conn);
766
+ conn.connect();
767
+ this.localConnections.set(deviceID, conn);
768
+ return conn;
769
+ }
770
+ /**
771
+ * After a Zigbee gateway connects, configure all its child sub-devices.
772
+ * Each child gets a lightweight LocalDevice whose update() call delegates
773
+ * to the parent, with the CID embedded in the payload.
774
+ *
775
+ * If a child connection already exists (e.g. after a gateway reconnect),
776
+ * re-link it to the parent and restore its connected state.
777
+ */
778
+ _setupZigbeeChildren(parentId, parentConn) {
779
+ const rel = this.gatewayRelationships.get(parentId);
780
+ if (!rel) {
781
+ return;
782
+ }
783
+ for (const entry of rel.children) {
784
+ const existingConn = this.localConnections.get(entry.deviceId);
785
+ if (existingConn && existingConn.parentDevice) {
786
+ // Child connection already exists – re-bind to new parent connection
787
+ this._reconnectZigbeeChild(entry.deviceId, entry.cid, existingConn, parentConn);
788
+ }
789
+ else {
790
+ // New child – set up from scratch
791
+ this._setupZigbeeChild(entry.deviceId, entry.cid, parentConn);
792
+ }
793
+ }
794
+ }
795
+ /**
796
+ * Re-bind an existing Zigbee child connection to a newly reconnected parent gateway.
797
+ * Preserves the child's state and event listeners while updating the parent reference.
798
+ */
799
+ _reconnectZigbeeChild(childDeviceId, cid, childConn, newParentConn) {
800
+ const childTuyaDevice = this.localDevices.find(d => d.id === childDeviceId);
801
+ if (!childTuyaDevice) {
802
+ this.log.warn(`Zigbee child ${childDeviceId}: TuyaDevice not found during reconnect`);
803
+ return;
804
+ }
805
+ // Update parent reference and CID
806
+ childConn.parentDevice = newParentConn;
807
+ childConn.childId = cid;
808
+ newParentConn.children.set(cid, childConn);
809
+ // Mark child as connected (mirrors parent state)
810
+ childConn.connected = true;
811
+ this.log.info(`Zigbee child ${childDeviceId} (CID=${cid}): re-linked to parent on reconnect`);
812
+ // Notify that device is now online
813
+ childTuyaDevice.online = true;
814
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: true });
815
+ // Request fresh state from child via parent
816
+ newParentConn.queryStateChild(cid);
817
+ }
818
+ /**
819
+ * Set up a single Zigbee child device.
820
+ * Registers the child's TuyaDevice (if not already done), links it to the
821
+ * parent LocalDevice connection, and subscribes to status updates.
822
+ */
823
+ _setupZigbeeChild(childDeviceId, cid, parentConn) {
824
+ const cfg = this.config.devices?.find(d => d.tuyaDeviceId === childDeviceId);
825
+ if (!cfg) {
826
+ this.log.warn(`Zigbee child config not found for device ${childDeviceId}`);
827
+ return;
828
+ }
829
+ // Check if child has per-child overrides for DP mapping or category
830
+ const effectiveChildCfg = { ...cfg };
831
+ if (cfg.childDpMapping) {
832
+ effectiveChildCfg.dpMapping = cfg.childDpMapping;
833
+ this.log.debug(`Zigbee child ${childDeviceId}: using per-child DP mapping override`);
834
+ }
835
+ if (cfg.childCategory) {
836
+ effectiveChildCfg.category = cfg.childCategory;
837
+ this.log.debug(`Zigbee child ${childDeviceId}: using per-child category override`);
838
+ }
839
+ // Register TuyaDevice if not already present
840
+ if (!this.localDevices.find(d => d.id === childDeviceId)) {
841
+ this._registerDeviceConfig(effectiveChildCfg);
842
+ }
843
+ const childTuyaDevice = this.localDevices.find(d => d.id === childDeviceId);
844
+ if (!childTuyaDevice) {
845
+ this.log.warn(`Could not build TuyaDevice for Zigbee child ${childDeviceId}`);
846
+ return;
847
+ }
848
+ // Create a lightweight LocalDevice for the child (no socket of its own)
849
+ const childConn = new LocalDevice_1.default({
850
+ id: childDeviceId,
851
+ key: parentConn['context'].key,
852
+ ip: parentConn['context'].ip,
853
+ version: parentConn['context'].version,
854
+ name: cfg.name ?? childDeviceId,
855
+ }, this.parentLog);
856
+ // Link parent ↔ child
857
+ childConn.parentDevice = parentConn;
858
+ childConn.childId = cid;
859
+ parentConn.children.set(cid, childConn);
860
+ // Mark child as connected immediately (it's online whenever parent is)
861
+ childConn.connected = true;
862
+ childConn.on('disconnect', () => {
863
+ childTuyaDevice.online = false;
864
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: false });
865
+ });
866
+ childConn.on('change', (changes) => {
867
+ const reverseMap = this.reverseDpMaps.get(childDeviceId) ?? {};
868
+ const statusUpdates = [];
869
+ for (const [dp, val] of Object.entries(changes)) {
870
+ const code = reverseMap[parseInt(dp, 10)] ?? dp;
871
+ const existing = childTuyaDevice.status.find(s => s.code === code);
872
+ if (existing) {
873
+ existing.value = val;
874
+ }
875
+ else {
876
+ childTuyaDevice.status.push({ code, value: val });
877
+ }
878
+ statusUpdates.push({ code, value: val });
879
+ }
880
+ if (statusUpdates.length > 0) {
881
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, childTuyaDevice, statusUpdates);
882
+ }
883
+ });
884
+ // Also handle the 'connect' event that parent propagation may emit
885
+ parentConn.on('connect', () => {
886
+ if (!childConn.connected) {
887
+ childConn.connected = true;
888
+ childTuyaDevice.online = true;
889
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: true });
890
+ // Refresh child state after gateway reconnects
891
+ parentConn.queryStateChild(cid);
892
+ }
893
+ });
894
+ this.localConnections.set(childDeviceId, childConn);
895
+ childTuyaDevice.online = true;
896
+ this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: true });
897
+ // Request child's current state immediately
898
+ parentConn.queryStateChild(cid);
899
+ this.log.info(`Zigbee child "${cfg.name ?? childDeviceId}" (${childDeviceId}, CID=${cid}) ` +
900
+ `connected via gateway ${parentConn['context'].id}`);
901
+ }
902
+ /**
903
+ * Discover and register Zigbee children from Tuya Cloud device list.
904
+ * If cloud API provides gateway_id field, we can auto-detect parent-child relationships
905
+ * and create child configs without manual config.
906
+ *
907
+ * @param cloudDeviceList - Optional array of cloud devices with id and gateway_id fields
908
+ */
909
+ discoverChildrenFromCloud(cloudDeviceList) {
910
+ if (!cloudDeviceList || cloudDeviceList.length === 0) {
911
+ return;
912
+ }
913
+ // Find parent-child relationships from cloud metadata
914
+ const gatewayToChildren = (0, DynamicChildDiscovery_1.discoverFromCloudList)(cloudDeviceList);
915
+ if (gatewayToChildren.size === 0) {
916
+ return;
917
+ }
918
+ // For each discovered parent-child relationship, ensure child config exists
919
+ for (const [gatewayDeviceId, childDeviceIds] of gatewayToChildren) {
920
+ const parentCfg = this.config.devices?.find(d => d.tuyaDeviceId === gatewayDeviceId);
921
+ if (!parentCfg) {
922
+ this.log.debug(`Cloud parent ${gatewayDeviceId}: no local config found, skipping auto-discovery`);
923
+ continue;
924
+ }
925
+ // Only auto-discover for gateways that support it
926
+ if (!(0, DynamicChildDiscovery_1.supportsChildDiscovery)(parentCfg)) {
927
+ continue;
928
+ }
929
+ for (const childDeviceId of childDeviceIds) {
930
+ // Check if child config already exists
931
+ const existingChildCfg = this.config.devices?.find(d => d.tuyaDeviceId === childDeviceId);
932
+ if (existingChildCfg) {
933
+ continue; // Already configured manually
934
+ }
935
+ // Auto-create child config from cloud metadata
936
+ // Use a placeholder CID (will be replaced by LAN discovery when gateway connects)
937
+ const placeholderCid = `00000000${childDeviceId.substring(0, 8).toLowerCase().padEnd(8, '0')}`;
938
+ const autoChildCfg = (0, DynamicChildDiscovery_1.buildDiscoveredChildConfig)(gatewayDeviceId, placeholderCid, undefined, parentCfg.ip, parentCfg.tuyaKey);
939
+ autoChildCfg.zigbeeChildId = undefined; // Will be discovered dynamically
940
+ if (!this.config.devices) {
941
+ this.config.devices = [];
942
+ }
943
+ this.config.devices.push(autoChildCfg);
944
+ this.log.info(`Zigbee child auto-discovery: registered ${childDeviceId} as child of ${gatewayDeviceId} ` +
945
+ '(from cloud metadata)');
946
+ }
947
+ }
948
+ // Re-detect Zigbee relationships now that we've added auto-discovered children
949
+ if (this.config.devices && this.config.devices.length > 0) {
950
+ try {
951
+ this.gatewayRelationships = ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.detectFromDevices(this.config.devices);
952
+ }
953
+ catch (err) {
954
+ const msg = err instanceof Error ? err.message : String(err);
955
+ this.log.warn(`Re-detection of Zigbee relationships after auto-discovery failed: ${msg}`);
956
+ }
957
+ }
958
+ }
959
+ /** Connect all configured local devices eagerly (called after init). */
960
+ connectAllDevices() {
961
+ for (const device of this.localDevices) {
962
+ const localKey = device.localKey;
963
+ if (!localKey) {
964
+ continue; // Skip if no local key configured
965
+ }
966
+ // Only connect if device has an explicit IP in config
967
+ // Devices relying on auto-discovery will be connected later after discovery completes
968
+ const localIp = device.localIp;
969
+ if (!localIp) {
970
+ this.log.debug(`Skipping initial connection for ${device.name} (${device.id}): ` +
971
+ 'waiting for auto-discovery to find IP address');
972
+ continue;
973
+ }
974
+ if (device.ip) {
975
+ this._createConnection(device.id);
976
+ }
977
+ }
978
+ }
979
+ }
980
+ exports.default = LocalDeviceManager;
981
+ //# sourceMappingURL=LocalDeviceManager.js.map