@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,418 @@
1
+ import { describe, expect, it, beforeEach, jest } from '@jest/globals';
2
+ import {
3
+ extractZigbeeEvents,
4
+ setupZigbeeEventMonitoring,
5
+ isValidZigbeeEvent,
6
+ batchProcessZigbeeEvents,
7
+ logEventProcessingSummary,
8
+ ZigbeeDeviceEvent,
9
+ ZigbeeDeviceEventInfo,
10
+ } from '../../src/local/ZigbeeEventDetection';
11
+ import Logger from '../../src/shared/util/Logger';
12
+ import { EventEmitter } from 'events';
13
+
14
+ function makeMockLog(): Logger {
15
+ return {
16
+ debug: jest.fn(),
17
+ info: jest.fn(),
18
+ warn: jest.fn(),
19
+ error: jest.fn(),
20
+ log: jest.fn(),
21
+ } as unknown as Logger;
22
+ }
23
+
24
+ describe('ZigbeeEventDetection - Phase 5', () => {
25
+ let log: Logger;
26
+
27
+ beforeEach(() => {
28
+ log = makeMockLog();
29
+ });
30
+
31
+ describe('extractZigbeeEvents', () => {
32
+ it('detects join event from subdev_event field', () => {
33
+ const payload = {
34
+ subdev_event: 'join',
35
+ cid: '00112233445566aa',
36
+ };
37
+
38
+ const events = extractZigbeeEvents(payload, 'gw_001');
39
+
40
+ expect(events).toHaveLength(1);
41
+ expect(events[0].event).toBe(ZigbeeDeviceEvent.JOINED);
42
+ expect(events[0].cid).toBe('00112233445566aa');
43
+ expect(events[0].gatewayId).toBe('gw_001');
44
+ });
45
+
46
+ it('detects leave event from subdev_event field', () => {
47
+ const payload = {
48
+ subdev_event: 'leave',
49
+ cid: '00112233445566bb',
50
+ };
51
+
52
+ const events = extractZigbeeEvents(payload);
53
+
54
+ expect(events).toHaveLength(1);
55
+ expect(events[0].event).toBe(ZigbeeDeviceEvent.LEFT);
56
+ expect(events[0].cid).toBe('00112233445566bb');
57
+ });
58
+
59
+ it('handles numeric event codes (0=leave, 1=join)', () => {
60
+ const joinPayload = { device_join: 1, device_cid: '00112233445566aa' };
61
+ const leavePayload = { device_join: 0, device_cid: '00112233445566bb' };
62
+
63
+ const joinEvents = extractZigbeeEvents(joinPayload);
64
+ const leaveEvents = extractZigbeeEvents(leavePayload);
65
+
66
+ expect(joinEvents).toHaveLength(1);
67
+ expect(leaveEvents).toHaveLength(1);
68
+ expect(joinEvents[0].event).toBe(ZigbeeDeviceEvent.JOINED);
69
+ expect(leaveEvents[0].event).toBe(ZigbeeDeviceEvent.LEFT);
70
+ });
71
+
72
+ it('detects device_join field', () => {
73
+ const payload = {
74
+ device_join: true,
75
+ device_cid: '00aabbccddee1234',
76
+ };
77
+
78
+ const events = extractZigbeeEvents(payload);
79
+
80
+ expect(events).toHaveLength(1);
81
+ expect(events[0].event).toBe(ZigbeeDeviceEvent.JOINED);
82
+ expect(events[0].cid).toBe('00aabbccddee1234');
83
+ });
84
+
85
+ it('parses subdev_list format (array of children)', () => {
86
+ const payload = {
87
+ subdev_list: [
88
+ { cid: '00112233445566aa', online: 1, productName: 'Light' },
89
+ { cid: '00112233445566bb', online: 0, productName: 'Switch' },
90
+ ],
91
+ };
92
+
93
+ const events = extractZigbeeEvents(payload, 'gw_001');
94
+
95
+ expect(events).toHaveLength(2);
96
+ expect(events[0].event).toBe(ZigbeeDeviceEvent.JOINED);
97
+ expect(events[0].productName).toBe('Light');
98
+ expect(events[1].event).toBe(ZigbeeDeviceEvent.LEFT);
99
+ expect(events[1].productName).toBe('Switch');
100
+ });
101
+
102
+ it('parses child_device_list format (alternative name)', () => {
103
+ const payload = {
104
+ child_device_list: [
105
+ { cid: '11223344556600aa', online: true },
106
+ { cid: '11223344556600bb', online: false },
107
+ ],
108
+ };
109
+
110
+ const events = extractZigbeeEvents(payload);
111
+
112
+ expect(events).toHaveLength(2);
113
+ expect(events[0].cid).toBe('11223344556600aa');
114
+ expect(events[1].cid).toBe('11223344556600bb');
115
+ });
116
+
117
+ it('ignores list entries without CID', () => {
118
+ const payload = {
119
+ subdev_list: [
120
+ { cid: '00112233445566aa', online: 1 },
121
+ { online: 1 }, // No CID - should be ignored
122
+ { cid: '00112233445566cc', online: 1 },
123
+ ],
124
+ };
125
+
126
+ const events = extractZigbeeEvents(payload);
127
+
128
+ // Should only have 2 events (skipped the one without CID)
129
+ expect(events).toHaveLength(2);
130
+ expect(events.map(e => e.cid)).not.toContain(undefined);
131
+ });
132
+
133
+ it('returns empty array for empty payload', () => {
134
+ const events = extractZigbeeEvents({});
135
+ expect(events).toEqual([]);
136
+ });
137
+
138
+ it('returns empty array when no recognized event fields', () => {
139
+ const payload = { some_other_field: 'value' };
140
+ const events = extractZigbeeEvents(payload);
141
+ expect(events).toEqual([]);
142
+ });
143
+
144
+ it('extracts CID from various field names', () => {
145
+ const withCid = extractZigbeeEvents({
146
+ subdev_event: 'join',
147
+ cid: '00112233445566aa',
148
+ });
149
+ expect(withCid[0].cid).toBe('00112233445566aa');
150
+
151
+ const withDeviceCid = extractZigbeeEvents({
152
+ device_join: true,
153
+ device_cid: '00112233445566bb',
154
+ });
155
+ expect(withDeviceCid[0].cid).toBe('00112233445566bb');
156
+
157
+ const withChildCid = extractZigbeeEvents({
158
+ subdev_event: 'leave',
159
+ child_cid: '00112233445566cc',
160
+ });
161
+ expect(withChildCid[0].cid).toBe('00112233445566cc');
162
+ });
163
+
164
+ it('includes timestamp in events', () => {
165
+ const payload = { subdev_event: 'join', cid: '00112233445566aa' };
166
+ const before = Date.now();
167
+ const events = extractZigbeeEvents(payload);
168
+ const after = Date.now();
169
+
170
+ expect(events[0].timestamp).toBeGreaterThanOrEqual(before);
171
+ expect(events[0].timestamp).toBeLessThanOrEqual(after);
172
+ });
173
+ });
174
+
175
+ describe('isValidZigbeeEvent', () => {
176
+ it('validates correct CID format (16 hex lowercase)', () => {
177
+ const event: ZigbeeDeviceEventInfo = {
178
+ event: ZigbeeDeviceEvent.JOINED,
179
+ cid: '00112233445566aa',
180
+ timestamp: Date.now(),
181
+ };
182
+
183
+ expect(isValidZigbeeEvent(event)).toBe(true);
184
+ });
185
+
186
+ it('rejects invalid CID format', () => {
187
+ const event: ZigbeeDeviceEventInfo = {
188
+ event: ZigbeeDeviceEvent.JOINED,
189
+ cid: 'INVALID',
190
+ timestamp: Date.now(),
191
+ };
192
+
193
+ expect(isValidZigbeeEvent(event)).toBe(false);
194
+ });
195
+
196
+ it('rejects uppercase CID', () => {
197
+ const event: ZigbeeDeviceEventInfo = {
198
+ event: ZigbeeDeviceEvent.JOINED,
199
+ cid: '00112233445566AA',
200
+ timestamp: Date.now(),
201
+ };
202
+
203
+ expect(isValidZigbeeEvent(event)).toBe(false);
204
+ });
205
+
206
+ it('rejects wrong-length CID', () => {
207
+ const event: ZigbeeDeviceEventInfo = {
208
+ event: ZigbeeDeviceEvent.JOINED,
209
+ cid: '00112233445566',
210
+ timestamp: Date.now(),
211
+ };
212
+
213
+ expect(isValidZigbeeEvent(event)).toBe(false);
214
+ });
215
+
216
+ it('accepts all valid event types', () => {
217
+ const events = [
218
+ ZigbeeDeviceEvent.JOINED,
219
+ ZigbeeDeviceEvent.LEFT,
220
+ ZigbeeDeviceEvent.ONLINE,
221
+ ZigbeeDeviceEvent.OFFLINE,
222
+ ];
223
+
224
+ for (const eventType of events) {
225
+ const event: ZigbeeDeviceEventInfo = {
226
+ event: eventType,
227
+ cid: '00112233445566aa',
228
+ timestamp: Date.now(),
229
+ };
230
+ expect(isValidZigbeeEvent(event)).toBe(true);
231
+ }
232
+ });
233
+ });
234
+
235
+ describe('batchProcessZigbeeEvents', () => {
236
+ it('groups events by joined/left', () => {
237
+ const events: ZigbeeDeviceEventInfo[] = [
238
+ {
239
+ event: ZigbeeDeviceEvent.JOINED,
240
+ cid: '00112233445566aa',
241
+ timestamp: Date.now(),
242
+ },
243
+ {
244
+ event: ZigbeeDeviceEvent.JOINED,
245
+ cid: '00112233445566bb',
246
+ timestamp: Date.now(),
247
+ },
248
+ {
249
+ event: ZigbeeDeviceEvent.LEFT,
250
+ cid: '00112233445566cc',
251
+ timestamp: Date.now(),
252
+ },
253
+ ];
254
+
255
+ const result = batchProcessZigbeeEvents(events, log);
256
+
257
+ expect(result.joined).toEqual(['00112233445566aa', '00112233445566bb']);
258
+ expect(result.left).toEqual(['00112233445566cc']);
259
+ });
260
+
261
+ it('filters out invalid events', () => {
262
+ const events: ZigbeeDeviceEventInfo[] = [
263
+ {
264
+ event: ZigbeeDeviceEvent.JOINED,
265
+ cid: '00112233445566aa',
266
+ timestamp: Date.now(),
267
+ },
268
+ {
269
+ event: ZigbeeDeviceEvent.JOINED,
270
+ cid: 'INVALID',
271
+ timestamp: Date.now(),
272
+ },
273
+ ];
274
+
275
+ const result = batchProcessZigbeeEvents(events, log);
276
+
277
+ expect(result.joined).toEqual(['00112233445566aa']);
278
+ const warnSpy = log.warn as jest.MockedFunction<typeof log.warn>;
279
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid Zigbee event'));
280
+ });
281
+
282
+ it('handles empty event array', () => {
283
+ const result = batchProcessZigbeeEvents([], log);
284
+
285
+ expect(result.joined).toEqual([]);
286
+ expect(result.left).toEqual([]);
287
+ });
288
+ });
289
+
290
+ describe('logEventProcessingSummary', () => {
291
+ it('logs event counts', () => {
292
+ logEventProcessingSummary(
293
+ ['00112233445566aa', '00112233445566bb'],
294
+ ['00112233445566cc'],
295
+ log,
296
+ );
297
+
298
+ const debugSpy = log.debug as jest.MockedFunction<typeof log.debug>;
299
+ const calls = debugSpy.mock.calls.map(c => String(c[0]));
300
+ expect(calls.some(c => c.includes('2 joined'))).toBe(true);
301
+ expect(calls.some(c => c.includes('1 left'))).toBe(true);
302
+ });
303
+
304
+ it('does not log when no events', () => {
305
+ logEventProcessingSummary([], [], log);
306
+
307
+ const debugSpy = log.debug as jest.MockedFunction<typeof log.debug>;
308
+ expect(debugSpy).not.toHaveBeenCalled();
309
+ });
310
+
311
+ it('lists CIDs in debug output', () => {
312
+ logEventProcessingSummary(
313
+ ['00112233445566aa', '00112233445566bb'],
314
+ ['00112233445566cc'],
315
+ log,
316
+ );
317
+
318
+ const debugSpy = log.debug as jest.MockedFunction<typeof log.debug>;
319
+ const calls = debugSpy.mock.calls.map(c => String(c[0]));
320
+
321
+ const joinedCall = calls.find(c => c.includes('Joined'));
322
+ expect(joinedCall).toContain('00112233445566aa');
323
+ expect(joinedCall).toContain('00112233445566bb');
324
+
325
+ const leftCall = calls.find(c => c.includes('Left'));
326
+ expect(leftCall).toContain('00112233445566cc');
327
+ });
328
+ });
329
+
330
+ describe('setupZigbeeEventMonitoring', () => {
331
+ it('sets up change listener and calls callback on events', () => {
332
+ const mockGateway = new EventEmitter();
333
+ const callback = jest.fn();
334
+
335
+ setupZigbeeEventMonitoring(mockGateway as any, 'gw_001', callback, log);
336
+
337
+ // Emit a change event with join
338
+ mockGateway.emit('change', {
339
+ subdev_event: 'join',
340
+ cid: '00112233445566aa',
341
+ });
342
+
343
+ expect(callback).toHaveBeenCalledWith(
344
+ expect.objectContaining({
345
+ event: ZigbeeDeviceEvent.JOINED,
346
+ cid: '00112233445566aa',
347
+ gatewayId: 'gw_001',
348
+ }),
349
+ );
350
+ });
351
+
352
+ it('logs event when detected', () => {
353
+ const mockGateway = new EventEmitter();
354
+ const callback = jest.fn();
355
+
356
+ setupZigbeeEventMonitoring(mockGateway as any, 'gw_001', callback, log);
357
+
358
+ mockGateway.emit('change', {
359
+ subdev_event: 'leave',
360
+ cid: '00112233445566aa',
361
+ });
362
+
363
+ const infoSpy = log.info as jest.MockedFunction<typeof log.info>;
364
+ expect(infoSpy).toHaveBeenCalledWith(
365
+ expect.stringContaining('child device left'),
366
+ );
367
+ });
368
+
369
+ it('returns cleanup function that removes listener', () => {
370
+ const mockGateway = new EventEmitter();
371
+ const callback = jest.fn();
372
+
373
+ const cleanup = setupZigbeeEventMonitoring(mockGateway as any, 'gw_001', callback, log);
374
+
375
+ // Emit before cleanup
376
+ mockGateway.emit('change', { subdev_event: 'join', cid: '00112233445566aa' });
377
+ expect(callback).toHaveBeenCalledTimes(1);
378
+
379
+ // Call cleanup
380
+ cleanup();
381
+
382
+ // Emit after cleanup
383
+ mockGateway.emit('change', { subdev_event: 'join', cid: '00112233445566bb' });
384
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1, callback not called again
385
+ });
386
+
387
+ it('handles multiple events in single change', () => {
388
+ const mockGateway = new EventEmitter();
389
+ const callback = jest.fn();
390
+
391
+ setupZigbeeEventMonitoring(mockGateway as any, 'gw_001', callback, log);
392
+
393
+ // Emit with subdev_list (multiple children)
394
+ mockGateway.emit('change', {
395
+ subdev_list: [
396
+ { cid: '00112233445566aa', online: 1 },
397
+ { cid: '00112233445566bb', online: 0 },
398
+ ],
399
+ });
400
+
401
+ expect(callback).toHaveBeenCalledTimes(2);
402
+ });
403
+
404
+ it('respects gateway ID in logging', () => {
405
+ const mockGateway = new EventEmitter();
406
+ const callback = jest.fn();
407
+
408
+ setupZigbeeEventMonitoring(mockGateway as any, 'custom_gw', callback, log);
409
+
410
+ mockGateway.emit('change', { subdev_event: 'join', cid: '00112233445566aa' });
411
+
412
+ const infoSpy = log.info as jest.MockedFunction<typeof log.info>;
413
+ expect(infoSpy).toHaveBeenCalledWith(
414
+ expect.stringContaining('custom_gw'),
415
+ );
416
+ });
417
+ });
418
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { ZigbeeGatewayDetection } from '../../src/local/ZigbeeGatewayDetection';
3
+ import { LocalDeviceConfig } from '../../src/local/config';
4
+
5
+ // Minimal helper to build a LocalDeviceConfig for testing
6
+ function device(
7
+ tuyaDeviceId: string,
8
+ overrides: Partial<LocalDeviceConfig> = {},
9
+ ): LocalDeviceConfig {
10
+ return {
11
+ tuyaDeviceId,
12
+ tuyaKey: 'localkey12345678',
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe('ZigbeeGatewayDetection', () => {
18
+
19
+ // ── detectFromDevices ─────────────────────────────────────────────────────
20
+
21
+ describe('detectFromDevices', () => {
22
+
23
+ it('returns an empty map when there are no devices', () => {
24
+ expect(ZigbeeGatewayDetection.detectFromDevices([])).toEqual(new Map());
25
+ });
26
+
27
+ it('returns an empty map when no device has a parentDeviceId', () => {
28
+ const devices = [device('gw_1'), device('gw_2')];
29
+ expect(ZigbeeGatewayDetection.detectFromDevices(devices).size).toBe(0);
30
+ });
31
+
32
+ it('detects a single parent with one child', () => {
33
+ const devices = [
34
+ device('gw_1'),
35
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601' }),
36
+ ];
37
+ const result = ZigbeeGatewayDetection.detectFromDevices(devices);
38
+ expect(result.size).toBe(1);
39
+ const rel = result.get('gw_1');
40
+ expect(rel).toBeDefined();
41
+ expect(rel!.parentId).toBe('gw_1');
42
+ expect(rel!.children).toHaveLength(1);
43
+ expect(rel!.children[0]).toMatchObject({
44
+ deviceId: 'child_1',
45
+ cid: '0011223344556601',
46
+ });
47
+ });
48
+
49
+ it('stores CID in lowercase', () => {
50
+ const devices = [
51
+ device('gw_1'),
52
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: 'AABBCCDDEEFF0011' }),
53
+ ];
54
+ const rel = ZigbeeGatewayDetection.detectFromDevices(devices).get('gw_1');
55
+ expect(rel!.children[0].cid).toBe('aabbccddeeff0011');
56
+ });
57
+
58
+ it('detects multiple children under the same gateway', () => {
59
+ const devices = [
60
+ device('gw_1'),
61
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601', name: 'Light A' }),
62
+ device('child_2', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556602', name: 'Light B' }),
63
+ ];
64
+ const rel = ZigbeeGatewayDetection.detectFromDevices(devices).get('gw_1');
65
+ expect(rel!.children).toHaveLength(2);
66
+ });
67
+
68
+ it('detects two separate gateways each with their own children', () => {
69
+ const devices = [
70
+ device('gw_A'),
71
+ device('gw_B'),
72
+ device('c_A1', { parentDeviceId: 'gw_A', zigbeeChildId: '0011223344556601' }),
73
+ device('c_B1', { parentDeviceId: 'gw_B', zigbeeChildId: '0011223344556602' }),
74
+ ];
75
+ const result = ZigbeeGatewayDetection.detectFromDevices(devices);
76
+ expect(result.size).toBe(2);
77
+ expect(result.get('gw_A')!.children[0].deviceId).toBe('c_A1');
78
+ expect(result.get('gw_B')!.children[0].deviceId).toBe('c_B1');
79
+ });
80
+
81
+ it('uses the device name field for the child entry name', () => {
82
+ const devices = [
83
+ device('gw_1'),
84
+ device('child_1', {
85
+ parentDeviceId: 'gw_1',
86
+ zigbeeChildId: '0011223344556601',
87
+ name: 'Kitchen Light',
88
+ }),
89
+ ];
90
+ const rel = ZigbeeGatewayDetection.detectFromDevices(devices).get('gw_1');
91
+ expect(rel!.children[0].name).toBe('Kitchen Light');
92
+ });
93
+
94
+ it('falls back to tuyaDeviceId as name when name is absent', () => {
95
+ const devices = [
96
+ device('gw_1'),
97
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601' }),
98
+ ];
99
+ const rel = ZigbeeGatewayDetection.detectFromDevices(devices).get('gw_1');
100
+ expect(rel!.children[0].name).toBe('child_1');
101
+ });
102
+
103
+ });
104
+
105
+ // ── validation errors ─────────────────────────────────────────────────────
106
+
107
+ describe('detectFromDevices – validation errors', () => {
108
+
109
+ it('throws when parentDeviceId is set without zigbeeChildId', () => {
110
+ const devices = [
111
+ device('gw_1'),
112
+ device('child_1', { parentDeviceId: 'gw_1' }),
113
+ ];
114
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
115
+ .toThrow('parentDeviceId but is missing zigbeeChildId');
116
+ });
117
+
118
+ it('throws when zigbeeChildId is set without parentDeviceId', () => {
119
+ const devices = [device('child_1', { zigbeeChildId: '0011223344556601' })];
120
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
121
+ .toThrow('zigbeeChildId but is missing parentDeviceId');
122
+ });
123
+
124
+ it('throws for an invalid CID (too short)', () => {
125
+ const devices = [
126
+ device('gw_1'),
127
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '001122' }),
128
+ ];
129
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
130
+ .toThrow('invalid zigbeeChildId');
131
+ });
132
+
133
+ it('throws for an invalid CID (non-hex characters)', () => {
134
+ const devices = [
135
+ device('gw_1'),
136
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: 'ZZZZZZZZZZZZZZZZ' }),
137
+ ];
138
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
139
+ .toThrow('invalid zigbeeChildId');
140
+ });
141
+
142
+ it('throws when the referenced parent device is not in the list', () => {
143
+ const devices = [
144
+ device('child_1', { parentDeviceId: 'missing_gw', zigbeeChildId: '0011223344556601' }),
145
+ ];
146
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
147
+ .toThrow('was not found in the local devices list');
148
+ });
149
+
150
+ it('throws when two children under the same gateway share a CID', () => {
151
+ const sharedCid = '0011223344556601';
152
+ const devices = [
153
+ device('gw_1'),
154
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: sharedCid }),
155
+ device('child_2', { parentDeviceId: 'gw_1', zigbeeChildId: sharedCid }),
156
+ ];
157
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
158
+ .toThrow('Duplicate Zigbee CID');
159
+ });
160
+
161
+ it('throws when a child is itself used as a parent (chaining)', () => {
162
+ const devices = [
163
+ device('gw_1'),
164
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601' }),
165
+ device('grand_child', { parentDeviceId: 'child_1', zigbeeChildId: '0011223344556602' }),
166
+ ];
167
+ expect(() => ZigbeeGatewayDetection.detectFromDevices(devices))
168
+ .toThrow('Chained gateways are not supported');
169
+ });
170
+
171
+ });
172
+
173
+ // ── isChild ───────────────────────────────────────────────────────────────
174
+
175
+ describe('isChild', () => {
176
+ it('returns true when parentDeviceId is set', () => {
177
+ const cfg = device('d1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601' });
178
+ expect(ZigbeeGatewayDetection.isChild(cfg)).toBe(true);
179
+ });
180
+
181
+ it('returns false when parentDeviceId is absent', () => {
182
+ const cfg = device('d1');
183
+ expect(ZigbeeGatewayDetection.isChild(cfg)).toBe(false);
184
+ });
185
+
186
+ it('returns false for a standalone device with only tuyaKey', () => {
187
+ const cfg = device('d1', { tuyaKey: 'somekey123456789' });
188
+ expect(ZigbeeGatewayDetection.isChild(cfg)).toBe(false);
189
+ });
190
+ });
191
+
192
+ // ── isGateway ─────────────────────────────────────────────────────────────
193
+
194
+ describe('isGateway', () => {
195
+ it('returns true when isZigbeeGateway is explicitly set', () => {
196
+ const allDevices = [device('gw_1', { isZigbeeGateway: true })];
197
+ expect(ZigbeeGatewayDetection.isGateway(allDevices[0], allDevices)).toBe(true);
198
+ });
199
+
200
+ it('returns true when another device references this one as parent', () => {
201
+ const allDevices = [
202
+ device('gw_1'),
203
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601' }),
204
+ ];
205
+ expect(ZigbeeGatewayDetection.isGateway(allDevices[0], allDevices)).toBe(true);
206
+ });
207
+
208
+ it('returns false for a standalone device with no children', () => {
209
+ const allDevices = [device('standalone_1'), device('standalone_2')];
210
+ expect(ZigbeeGatewayDetection.isGateway(allDevices[0], allDevices)).toBe(false);
211
+ });
212
+
213
+ it('returns false for a child device that has no children of its own', () => {
214
+ const allDevices = [
215
+ device('gw_1'),
216
+ device('child_1', { parentDeviceId: 'gw_1', zigbeeChildId: '0011223344556601' }),
217
+ ];
218
+ expect(ZigbeeGatewayDetection.isGateway(allDevices[1], allDevices)).toBe(false);
219
+ });
220
+ });
221
+
222
+ });
@@ -0,0 +1 @@
1
+ 85778e1add80c517
@@ -0,0 +1 @@
1
+ 35d2f400f4ef8c0f