@homebridge-plugins/homebridge-tuya 2.2.1 → 3.0.0-beta.1

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 +161 -144
  3. package/config.schema.json +976 -11
  4. package/dist/{core → cloud/api}/TuyaOpenAPI.js +6 -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 +8 -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 +833 -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 +149 -69
  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 +99 -30
  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 +22 -27
  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 +268 -0
  167. package/test/mqttMessages.test.ts +495 -0
  168. package/test/platform.test.ts +515 -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,274 @@
1
+ /// <reference types="node" />
2
+ import { describe, expect, it, beforeEach, jest } from '@jest/globals';
3
+ import EventEmitter from 'events';
4
+ import LocalDevice, { LocalDeviceContext } from '../../src/local/LocalDevice';
5
+
6
+ // ── Logger mock ───────────────────────────────────────────────────────────────
7
+
8
+ jest.mock('../../src/shared/util/Logger', () => ({
9
+ __esModule: true,
10
+ default: class Logger {
11
+ log() {}; info() {}; warn() {}; error() {}; debug() {};
12
+ },
13
+ PrefixLogger: class PrefixLogger {
14
+ constructor(public _log: any, public prefix: string) {}
15
+ debug() {}; info() {}; warn() {}; error() {};
16
+ },
17
+ }));
18
+
19
+ // ── Protocol mock ──────────────────────────────────────────────────────────────
20
+
21
+ jest.mock('../../src/local/protocol/ProtocolFactory', () => ({
22
+ ProtocolFactory: {
23
+ createProtocol: jest.fn(() => ({
24
+ isFrameComplete: jest.fn(() => false),
25
+ extractFrame: jest.fn(() => null),
26
+ encodeFrame: jest.fn(() => Buffer.alloc(0)),
27
+ decodeFrame: jest.fn(() => null),
28
+ })),
29
+ },
30
+ }));
31
+
32
+ // ── net mock ───────────────────────────────────────────────────────────────────
33
+
34
+ jest.mock('net', () => ({
35
+ createConnection: jest.fn(() => {
36
+ const s = new EventEmitter() as any;
37
+ s.setKeepAlive = jest.fn();
38
+ s.setNoDelay = jest.fn();
39
+ s.write = jest.fn();
40
+ s.destroy = jest.fn();
41
+ s.removeAllListeners = jest.fn(() => { s.removeAllListeners(); });
42
+ // Don't actually call removeAllListeners on EventEmitter to avoid recursion
43
+ s.removeAllListeners = jest.fn();
44
+ return s;
45
+ }),
46
+ }));
47
+
48
+ // ── Helpers ────────────────────────────────────────────────────────────────────
49
+
50
+ function makeLogger() {
51
+ return { log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() } as any;
52
+ }
53
+
54
+ function makeParent(overrides: Partial<LocalDeviceContext> = {}): LocalDevice {
55
+ const ctx: LocalDeviceContext = {
56
+ id: 'gateway_001',
57
+ key: Buffer.from('0123456789abcdef'),
58
+ ip: '192.168.1.100',
59
+ version: '3.3',
60
+ name: 'Test Gateway',
61
+ ...overrides,
62
+ };
63
+ return new LocalDevice(ctx, makeLogger());
64
+ }
65
+
66
+ function makeChild(parent: LocalDevice, cid: string): LocalDevice {
67
+ const ctx: LocalDeviceContext = {
68
+ id: 'child_001',
69
+ key: Buffer.from('0123456789abcdef'),
70
+ ip: '192.168.1.100',
71
+ version: '3.3',
72
+ name: 'Test Child',
73
+ };
74
+ const child = new LocalDevice(ctx, makeLogger());
75
+ child.parentDevice = parent;
76
+ child.childId = cid;
77
+ parent.children.set(cid, child);
78
+ return child;
79
+ }
80
+
81
+ // ── Tests ──────────────────────────────────────────────────────────────────────
82
+
83
+ describe('LocalDevice – Zigbee parent/child', () => {
84
+
85
+ describe('children Map', () => {
86
+ it('starts as an empty Map', () => {
87
+ const parent = makeParent();
88
+ expect(parent.children.size).toBe(0);
89
+ });
90
+
91
+ it('can register a child', () => {
92
+ const parent = makeParent();
93
+ const child = makeChild(parent, '0011223344556601');
94
+ expect(parent.children.has('0011223344556601')).toBe(true);
95
+ expect(child.parentDevice).toBe(parent);
96
+ expect(child.childId).toBe('0011223344556601');
97
+ });
98
+
99
+ it('can register multiple children with different CIDs', () => {
100
+ const parent = makeParent();
101
+ makeChild(parent, '0011223344556601');
102
+ makeChild(parent, '0011223344556602');
103
+ expect(parent.children.size).toBe(2);
104
+ });
105
+ });
106
+
107
+ describe('update() routing', () => {
108
+ it('calls updateChild on parent when this is a child', () => {
109
+ const parent = makeParent();
110
+ const child = makeChild(parent, '0011223344556601');
111
+ const spy = jest.spyOn(parent, 'updateChild');
112
+
113
+ child.update({ '1': true });
114
+
115
+ expect(spy).toHaveBeenCalledWith('0011223344556601', { '1': true });
116
+ });
117
+
118
+ it('does NOT call updateChild on parent for a standalone device', () => {
119
+ const standalone = makeParent();
120
+ const spy = jest.spyOn(standalone, 'updateChild');
121
+
122
+ standalone.update({ '1': true }); // no-op (no socket), but should not delegate
123
+
124
+ expect(spy).not.toHaveBeenCalled();
125
+ });
126
+ });
127
+
128
+ describe('updateChild()', () => {
129
+ it('logs a warning and returns for an invalid CID', () => {
130
+ const parent = makeParent();
131
+ const warnSpy = jest.spyOn(parent.log, 'warn');
132
+
133
+ parent.updateChild('bad-cid', { '1': true });
134
+
135
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid CID'));
136
+ });
137
+
138
+ it('logs a warning for unsupported protocol versions', () => {
139
+ const parent = makeParent({ version: '3.1' });
140
+ const warnSpy = jest.spyOn(parent.log, 'warn');
141
+
142
+ parent.updateChild('0011223344556601', { '1': true });
143
+
144
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('not supported for Zigbee'));
145
+ });
146
+
147
+ it('does not throw for a valid v3.3 gateway (even without socket)', () => {
148
+ const parent = makeParent({ version: '3.3' });
149
+ expect(() => parent.updateChild('0011223344556601', { '1': true })).not.toThrow();
150
+ });
151
+
152
+ it('does not throw for a valid v3.4 gateway', () => {
153
+ const parent = makeParent({ version: '3.4' });
154
+ expect(() => parent.updateChild('0011223344556601', { '1': true })).not.toThrow();
155
+ });
156
+
157
+ it('does not throw for a valid v3.5 gateway', () => {
158
+ const parent = makeParent({ version: '3.5' });
159
+ expect(() => parent.updateChild('0011223344556601', { '1': true })).not.toThrow();
160
+ });
161
+ });
162
+
163
+ describe('queryStateChild()', () => {
164
+ it('logs a warning for an invalid CID', () => {
165
+ const parent = makeParent();
166
+ const warnSpy = jest.spyOn(parent.log, 'warn');
167
+
168
+ parent.queryStateChild('bad');
169
+
170
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid CID'));
171
+ });
172
+
173
+ it('does not throw for a valid CID on v3.3 gateway', () => {
174
+ const parent = makeParent({ version: '3.3' });
175
+ expect(() => parent.queryStateChild('0011223344556601')).not.toThrow();
176
+ });
177
+ });
178
+
179
+ describe('_change() routing (child state update from gateway)', () => {
180
+ it('emits change on the correct child when CID matches', done => {
181
+ const parent = makeParent();
182
+ const child = makeChild(parent, '0011223344556601');
183
+
184
+ child.on('change', (changes: Record<string, unknown>) => {
185
+ expect(changes['1']).toBe(true);
186
+ done();
187
+ });
188
+
189
+ // Simulate parent routing child state update
190
+ child._change({ '1': true });
191
+ });
192
+
193
+ it('updates child.state accumulator', () => {
194
+ const parent = makeParent();
195
+ const child = makeChild(parent, '0011223344556601');
196
+
197
+ child._change({ '1': true, '2': 80 });
198
+
199
+ expect(child.state).toMatchObject({ '1': true, '2': 80 });
200
+ });
201
+
202
+ it('emits change only for values that actually changed', done => {
203
+ const parent = makeParent();
204
+ const child = makeChild(parent, '0011223344556601');
205
+ child.state = { '1': true }; // pre-set state
206
+
207
+ child.on('change', (changes: Record<string, unknown>) => {
208
+ // Only '2' changed
209
+ expect(Object.keys(changes)).toEqual(['2']);
210
+ done();
211
+ });
212
+
213
+ child._change({ '1': true, '2': 50 }); // '1' unchanged, '2' new
214
+ });
215
+
216
+ it('does not emit change when no values differ', () => {
217
+ const parent = makeParent();
218
+ const child = makeChild(parent, '0011223344556601');
219
+ child.state = { '1': true };
220
+
221
+ const spy = jest.fn();
222
+ child.on('change', spy);
223
+
224
+ child._change({ '1': true }); // same value
225
+
226
+ expect(spy).not.toHaveBeenCalled();
227
+ });
228
+ });
229
+
230
+ describe('disconnect() child propagation', () => {
231
+ it('emits disconnect on each registered child', done => {
232
+ const parent = makeParent();
233
+ const child1 = makeChild(parent, '0011223344556601');
234
+ const child2 = makeChild(parent, '0011223344556602');
235
+
236
+ child1.connected = true;
237
+ child2.connected = true;
238
+
239
+ let count = 0;
240
+ const onDisconnect = () => {
241
+ count++;
242
+ if (count === 2) done();
243
+ };
244
+
245
+ child1.on('disconnect', onDisconnect);
246
+ child2.on('disconnect', onDisconnect);
247
+
248
+ parent.disconnect();
249
+ });
250
+
251
+ it('sets connected=false on children', () => {
252
+ const parent = makeParent();
253
+ const child = makeChild(parent, '0011223344556601');
254
+ child.connected = true;
255
+
256
+ parent.disconnect();
257
+
258
+ expect(child.connected).toBe(false);
259
+ });
260
+
261
+ it('does not emit disconnect for children already disconnected', () => {
262
+ const parent = makeParent();
263
+ const child = makeChild(parent, '0011223344556601');
264
+ child.connected = false; // already disconnected
265
+
266
+ const spy = jest.fn();
267
+ child.on('disconnect', spy);
268
+
269
+ parent.disconnect();
270
+
271
+ expect(spy).not.toHaveBeenCalled();
272
+ });
273
+ });
274
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import LocalDeviceManager from '../../src/local/LocalDeviceManager';
3
+ import { LocalConfig } from '../../src/local/config';
4
+ import Logger from '../../src/shared/util/Logger';
5
+
6
+ function makeMockLog(): Logger {
7
+ return {
8
+ debug: jest.fn(),
9
+ info: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ log: jest.fn(),
13
+ } as unknown as Logger;
14
+ }
15
+
16
+ describe('LocalDeviceManager – Cloud-based child discovery', () => {
17
+ let log: Logger;
18
+ let manager: LocalDeviceManager;
19
+
20
+ const GATEWAY_ID = 'cloud_gw_001';
21
+ const DEVICE_KEY = '0123456789abcdef';
22
+ const DEVICE_IP = '192.168.1.200';
23
+
24
+ beforeEach(() => {
25
+ log = makeMockLog();
26
+ });
27
+
28
+ afterEach(() => {
29
+ if (manager) {
30
+ manager.stopLocalDevices();
31
+ }
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+ it('method exists and can be called without error', async () => {
36
+ const config: LocalConfig = {
37
+ autoDiscoverDevices: false,
38
+ devices: [
39
+ {
40
+ tuyaDeviceId: GATEWAY_ID,
41
+ name: 'Cloud Gateway',
42
+ ip: DEVICE_IP,
43
+ tuyaKey: DEVICE_KEY,
44
+ isZigbeeGateway: true,
45
+ },
46
+ ],
47
+ };
48
+
49
+ manager = new LocalDeviceManager(config, log);
50
+
51
+ const cloudDevices = [
52
+ { id: GATEWAY_ID },
53
+ { id: 'child_cloud_001', gateway_id: GATEWAY_ID },
54
+ ];
55
+
56
+ // Should not throw
57
+ expect(() => {
58
+ manager.discoverChildrenFromCloud(cloudDevices);
59
+ }).not.toThrow();
60
+ });
61
+
62
+ it('logs debug for missing gateway config', async () => {
63
+ const config: LocalConfig = {
64
+ autoDiscoverDevices: false,
65
+ devices: [],
66
+ };
67
+
68
+ manager = new LocalDeviceManager(config, log);
69
+
70
+ const cloudDevices = [
71
+ { id: 'unknown_gw', gateway_id: undefined },
72
+ { id: 'child_001', gateway_id: 'unknown_gw' },
73
+ ];
74
+
75
+ manager.discoverChildrenFromCloud(cloudDevices);
76
+
77
+ const debugSpy = log.debug as jest.MockedFunction<typeof log.debug>;
78
+ expect(debugSpy.mock.calls.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ it('handles empty cloud device list', async () => {
82
+ const config: LocalConfig = {
83
+ autoDiscoverDevices: false,
84
+ devices: [
85
+ {
86
+ tuyaDeviceId: GATEWAY_ID,
87
+ name: 'Gateway',
88
+ ip: DEVICE_IP,
89
+ tuyaKey: DEVICE_KEY,
90
+ },
91
+ ],
92
+ };
93
+
94
+ manager = new LocalDeviceManager(config, log);
95
+
96
+ // Should not throw
97
+ expect(() => {
98
+ manager.discoverChildrenFromCloud([]);
99
+ manager.discoverChildrenFromCloud(undefined);
100
+ manager.discoverChildrenFromCloud(null as any);
101
+ }).not.toThrow();
102
+ });
103
+
104
+ it('respects isZigbeeGateway: false', async () => {
105
+ const config: LocalConfig = {
106
+ autoDiscoverDevices: false,
107
+ devices: [
108
+ {
109
+ tuyaDeviceId: GATEWAY_ID,
110
+ name: 'Non-Gateway',
111
+ ip: DEVICE_IP,
112
+ tuyaKey: DEVICE_KEY,
113
+ isZigbeeGateway: false,
114
+ },
115
+ ],
116
+ };
117
+
118
+ manager = new LocalDeviceManager(config, log);
119
+
120
+ const cloudDevices = [
121
+ { id: GATEWAY_ID },
122
+ { id: 'child_001', gateway_id: GATEWAY_ID },
123
+ ];
124
+
125
+ manager.discoverChildrenFromCloud(cloudDevices);
126
+
127
+ // Should not log auto-discovery (device is not a gateway)
128
+ const infoSpy = log.info as jest.MockedFunction<typeof log.info>;
129
+ const callsWithAutoDiscovery = infoSpy.mock.calls.filter(c =>
130
+ String(c[0]).includes('auto-discovery'),
131
+ );
132
+ expect(callsWithAutoDiscovery.length).toBe(0);
133
+ });
134
+
135
+ it('triggers Zigbee relationship re-detection', async () => {
136
+ const config: LocalConfig = {
137
+ autoDiscoverDevices: false,
138
+ devices: [
139
+ {
140
+ tuyaDeviceId: GATEWAY_ID,
141
+ name: 'Gateway',
142
+ ip: DEVICE_IP,
143
+ tuyaKey: DEVICE_KEY,
144
+ isZigbeeGateway: true,
145
+ },
146
+ ],
147
+ };
148
+
149
+ manager = new LocalDeviceManager(config, log);
150
+
151
+ const cloudDevices = [
152
+ { id: GATEWAY_ID },
153
+ { id: 'child_001', gateway_id: GATEWAY_ID },
154
+ ];
155
+
156
+ // After discovery, the config should have more devices (auto-discovered children added)
157
+ manager.discoverChildrenFromCloud(cloudDevices);
158
+
159
+ // The manager's internal gateway relationships should be re-detected
160
+ // (We can't easily verify this without breaking encapsulation, but the method should complete without error)
161
+ expect(manager).toBeDefined();
162
+ });
163
+ });
@@ -0,0 +1,200 @@
1
+ import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import LocalDeviceManager from '../../src/local/LocalDeviceManager';
3
+ import { LocalConfig, LocalDeviceConfig } from '../../src/local/config';
4
+ import TuyaDeviceManager from '../../src/cloud/device/TuyaDeviceManager';
5
+ import Logger from '../../src/shared/util/Logger';
6
+
7
+ function makeMockLog(): Logger {
8
+ return {
9
+ debug: jest.fn(),
10
+ info: jest.fn(),
11
+ warn: jest.fn(),
12
+ error: jest.fn(),
13
+ log: jest.fn(),
14
+ } as unknown as Logger;
15
+ }
16
+
17
+ describe('LocalDeviceManager – Zigbee gateway reconnection', () => {
18
+ let log: Logger;
19
+ let manager: LocalDeviceManager;
20
+
21
+ const GATEWAY_ID = 'gw_reconnect_001';
22
+ const CHILD_ID = 'child_reconnect_001';
23
+ const CHILD_CID = '00aabbccddee1234';
24
+ const DEVICE_KEY = '0123456789abcdef';
25
+ const DEVICE_IP = '192.168.1.200';
26
+
27
+ function zigbeeReconnectConfig(): LocalConfig {
28
+ return {
29
+ autoDiscoverDevices: false,
30
+ devices: [
31
+ {
32
+ tuyaDeviceId: GATEWAY_ID,
33
+ name: 'Reconnect Gateway',
34
+ ip: DEVICE_IP,
35
+ tuyaKey: DEVICE_KEY,
36
+ protocolVersion: '3.3',
37
+ isZigbeeGateway: true,
38
+ },
39
+ {
40
+ tuyaDeviceId: CHILD_ID,
41
+ name: 'Reconnect Child',
42
+ ip: DEVICE_IP,
43
+ tuyaKey: DEVICE_KEY,
44
+ protocolVersion: '3.3',
45
+ parentDeviceId: GATEWAY_ID,
46
+ zigbeeChildId: CHILD_CID,
47
+ },
48
+ ],
49
+ };
50
+ }
51
+
52
+ beforeEach(() => {
53
+ log = makeMockLog();
54
+ manager = new LocalDeviceManager(zigbeeReconnectConfig(), log);
55
+ });
56
+
57
+ afterEach(() => {
58
+ manager.stopLocalDevices();
59
+ jest.clearAllMocks();
60
+ });
61
+
62
+ it('detects gateway relationships during init', async () => {
63
+ await manager.initLocalDevices();
64
+
65
+ const gatewayRels = (manager as any).gatewayRelationships as Map<string, any>;
66
+ expect(gatewayRels.has(GATEWAY_ID)).toBe(true);
67
+
68
+ const rel = gatewayRels.get(GATEWAY_ID);
69
+ expect(rel.children.length).toBe(1);
70
+ expect(rel.children[0].cid).toBe(CHILD_CID);
71
+ expect(rel.children[0].deviceId).toBe(CHILD_ID);
72
+ });
73
+
74
+ it('handles per-child DP mapping override if provided', async () => {
75
+ const configWithOverride: LocalConfig = {
76
+ autoDiscoverDevices: false,
77
+ devices: [
78
+ {
79
+ tuyaDeviceId: GATEWAY_ID,
80
+ name: 'Gateway',
81
+ ip: DEVICE_IP,
82
+ tuyaKey: DEVICE_KEY,
83
+ dpMapping: { 'switch_1': 1, 'bright_value': 2 },
84
+ },
85
+ {
86
+ tuyaDeviceId: CHILD_ID,
87
+ name: 'Child',
88
+ ip: DEVICE_IP,
89
+ tuyaKey: DEVICE_KEY,
90
+ parentDeviceId: GATEWAY_ID,
91
+ zigbeeChildId: CHILD_CID,
92
+ childDpMapping: { 'switch_1': 10, 'bright_value': 11 }, // Different DPs
93
+ },
94
+ ],
95
+ };
96
+
97
+ const mgr = new LocalDeviceManager(configWithOverride, log);
98
+ // Should initialize without error even with per-child DP overrides
99
+ await mgr.initLocalDevices();
100
+
101
+ // Verify that gateway relationships include the child
102
+ const gatewayRels = (mgr as any).gatewayRelationships as Map<string, any>;
103
+ const childRel = gatewayRels.get(GATEWAY_ID);
104
+ expect(childRel?.children.length).toBe(1);
105
+ expect(childRel.children[0].deviceId).toBe(CHILD_ID);
106
+ expect(childRel.children[0].cid).toBe(CHILD_CID);
107
+
108
+ mgr.stopLocalDevices();
109
+ });
110
+
111
+ it('handles per-child category override if provided', async () => {
112
+ const configWithCategoryOverride: LocalConfig = {
113
+ autoDiscoverDevices: false,
114
+ devices: [
115
+ {
116
+ tuyaDeviceId: GATEWAY_ID,
117
+ name: 'Gateway Hub',
118
+ ip: DEVICE_IP,
119
+ tuyaKey: DEVICE_KEY,
120
+ category: 'kit', // Hub/gateway category
121
+ },
122
+ {
123
+ tuyaDeviceId: CHILD_ID,
124
+ name: 'Child Light',
125
+ ip: DEVICE_IP,
126
+ tuyaKey: DEVICE_KEY,
127
+ parentDeviceId: GATEWAY_ID,
128
+ zigbeeChildId: CHILD_CID,
129
+ childCategory: 'dj', // Override: this child is a light
130
+ },
131
+ ],
132
+ };
133
+
134
+ const mgr = new LocalDeviceManager(configWithCategoryOverride, log);
135
+ await mgr.initLocalDevices();
136
+
137
+ const gwDevice = mgr.getDevice(GATEWAY_ID);
138
+ const childDevice = mgr.getDevice(CHILD_ID);
139
+
140
+ expect(gwDevice?.category).toBe('kit');
141
+ // childDevice won't have category set until parent connects (lazy registration)
142
+
143
+ mgr.stopLocalDevices();
144
+ });
145
+
146
+ it('logs gateway relationships and recognizes multi-child setup', async () => {
147
+ const multiChildConfig: LocalConfig = {
148
+ autoDiscoverDevices: false,
149
+ devices: [
150
+ {
151
+ tuyaDeviceId: GATEWAY_ID,
152
+ name: 'Multi-Child Gateway',
153
+ ip: DEVICE_IP,
154
+ tuyaKey: DEVICE_KEY,
155
+ isZigbeeGateway: true,
156
+ },
157
+ {
158
+ tuyaDeviceId: 'child_a',
159
+ name: 'Child A',
160
+ ip: DEVICE_IP,
161
+ tuyaKey: DEVICE_KEY,
162
+ parentDeviceId: GATEWAY_ID,
163
+ zigbeeChildId: '0011223344556601',
164
+ },
165
+ {
166
+ tuyaDeviceId: 'child_b',
167
+ name: 'Child B',
168
+ ip: DEVICE_IP,
169
+ tuyaKey: DEVICE_KEY,
170
+ parentDeviceId: GATEWAY_ID,
171
+ zigbeeChildId: '0011223344556602',
172
+ },
173
+ ],
174
+ };
175
+
176
+ const mgr = new LocalDeviceManager(multiChildConfig, log);
177
+ await mgr.initLocalDevices();
178
+
179
+ const infoSpy = log.info as jest.MockedFunction<typeof log.info>;
180
+ const infoCalls = infoSpy.mock.calls.map(c => String(c[0]));
181
+ const gatewayLine = infoCalls.find(s => s.includes('Zigbee gateway detected'));
182
+
183
+ expect(gatewayLine).toBeDefined();
184
+ expect(gatewayLine).toContain(GATEWAY_ID);
185
+ expect(gatewayLine).toContain('2 sub-device');
186
+
187
+ mgr.stopLocalDevices();
188
+ });
189
+
190
+ it('manages gateway connections separately from local connections', async () => {
191
+ await manager.initLocalDevices();
192
+
193
+ const localConnections = (manager as any).localConnections as Map<string, any>;
194
+ const gatewayConnections = (manager as any).gatewayConnections as Map<string, any>;
195
+
196
+ // Initially, no connections are open (no TCP connect called)
197
+ expect(localConnections.size).toBe(0);
198
+ expect(gatewayConnections.size).toBe(0);
199
+ });
200
+ });