@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,1310 @@
1
+ /* eslint-disable no-console */
2
+ import { describe, expect, test, beforeEach, jest } from '@jest/globals';
3
+ import { TuyaPlatform } from '../src/platform';
4
+
5
+ // Mock Homebridge API
6
+ const mockHAP = {
7
+ Service: {},
8
+ Characteristic: {},
9
+ uuid: {
10
+ generate: (name: string) => `${name}-uuid`,
11
+ },
12
+ };
13
+
14
+ const mockAPI = {
15
+ hap: mockHAP,
16
+ platformAccessory: jest.fn(),
17
+ registerAccessory: jest.fn(),
18
+ unregisterAccessory: jest.fn(),
19
+ on: jest.fn(),
20
+ } as any;
21
+
22
+ // Mock Logger
23
+ jest.mock('../src/shared/util/Logger', () => ({
24
+ __esModule: true,
25
+ default: class Logger {
26
+ log() {}
27
+ info() {}
28
+ warn() {}
29
+ error() {}
30
+ },
31
+ PrefixLogger: class PrefixLogger {
32
+ constructor(public log: any, public name: string, public debug: boolean) {}
33
+ },
34
+ }));
35
+
36
+ // Mock file system
37
+ jest.mock('fs', () => ({
38
+ readFileSync: jest.fn(),
39
+ writeFileSync: jest.fn(),
40
+ }));
41
+
42
+ // Mock Cloud components
43
+ jest.mock('../src/cloud/api/TuyaOpenAPI', () => {
44
+ return class MockTuyaOpenAPI {
45
+ log = console;
46
+ };
47
+ });
48
+
49
+ jest.mock('../src/cloud/api/TuyaOpenMQ', () => {
50
+ return class MockTuyaOpenMQ {};
51
+ });
52
+
53
+ jest.mock('../src/cloud/device/TuyaDeviceManager', () => {
54
+ return class MockTuyaDeviceManager {};
55
+ });
56
+
57
+ jest.mock('../src/cloud/device/TuyaCustomDeviceManager', () => {
58
+ return class MockTuyaCustomDeviceManager {};
59
+ });
60
+
61
+ jest.mock('../src/cloud/device/TuyaHomeDeviceManager', () => {
62
+ return class MockTuyaHomeDeviceManager {};
63
+ });
64
+
65
+ // Mock Local components
66
+ jest.mock('../src/local/LocalDeviceManager', () => {
67
+ return class MockLocalDeviceManager {};
68
+ });
69
+
70
+ // Mock Accessories
71
+ jest.mock('../src/shared/accessories/AccessoryFactory', () => {
72
+ return {
73
+ default: class MockAccessoryFactory {},
74
+ };
75
+ });
76
+
77
+ jest.mock('../src/shared/util/util', () => ({
78
+ sanitizeName: (name: string) => name.replace(/[^a-z0-9]/gi, '_'),
79
+ retry: jest.fn(async (fn: any) => fn()),
80
+ }));
81
+
82
+ describe('TuyaPlatform', () => {
83
+ let platform: TuyaPlatform;
84
+ let mockConfig: any;
85
+ let mockLog: any;
86
+
87
+ beforeEach(() => {
88
+ mockLog = {
89
+ debug: jest.fn(),
90
+ info: jest.fn(),
91
+ warn: jest.fn(),
92
+ error: jest.fn(),
93
+ };
94
+
95
+ mockConfig = {
96
+ platform: 'TuyaPlatform',
97
+ name: 'Tuya',
98
+ mode: 'cloud',
99
+ options: {
100
+ projectType: '2',
101
+ accessId: 'test_id',
102
+ accessKey: 'test_key',
103
+ countryCode: 1,
104
+ username: 'user@example.com',
105
+ password: 'password',
106
+ appSchema: 'tuyaSmart',
107
+ generateWeatherAccessory: false,
108
+ weatherAPI: '',
109
+ },
110
+ };
111
+
112
+ platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
113
+ });
114
+
115
+ describe('initialization', () => {
116
+ test('creates platform instance', () => {
117
+ expect(platform).toBeDefined();
118
+ expect(platform instanceof TuyaPlatform).toBe(true);
119
+ });
120
+
121
+ test('stores logger', () => {
122
+ expect(platform.log).toBe(mockLog);
123
+ });
124
+
125
+ test('stores config', () => {
126
+ expect(platform.config).toBe(mockConfig);
127
+ });
128
+
129
+ test('stores API', () => {
130
+ expect(platform.api).toBe(mockAPI);
131
+ });
132
+
133
+ test('has Service and Characteristic', () => {
134
+ expect(platform.Service).toBeDefined();
135
+ expect(platform.Characteristic).toBeDefined();
136
+ });
137
+
138
+ test('initializes cached accessories array', () => {
139
+ expect(platform.cachedAccessories).toEqual([]);
140
+ expect(Array.isArray(platform.cachedAccessories)).toBe(true);
141
+ });
142
+
143
+ test('initializes accessory handlers array', () => {
144
+ expect(platform.accessoryHandlers).toEqual([]);
145
+ expect(Array.isArray(platform.accessoryHandlers)).toBe(true);
146
+ });
147
+ });
148
+
149
+ describe('mode configuration', () => {
150
+ test('uses cloud mode by default', () => {
151
+ expect(platform.mode).toBe('cloud');
152
+ });
153
+
154
+ test('supports local mode', () => {
155
+ const localConfig = {
156
+ ...mockConfig,
157
+ mode: 'local',
158
+ options: undefined,
159
+ local: {
160
+ devices: [],
161
+ },
162
+ };
163
+
164
+ const localPlatform = new TuyaPlatform(mockLog, localConfig, mockAPI);
165
+ expect(localPlatform.mode).toBe('local');
166
+ });
167
+
168
+ test('supports both mode', () => {
169
+ const bothConfig = {
170
+ ...mockConfig,
171
+ mode: 'both',
172
+ local: {
173
+ devices: [],
174
+ },
175
+ };
176
+
177
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
178
+ expect(bothPlatform.mode).toBe('both');
179
+ });
180
+ });
181
+
182
+ describe('configuration validation', () => {
183
+ test('requires options for cloud mode', () => {
184
+ const noOptionsConfig = {
185
+ platform: 'TuyaPlatform',
186
+ name: 'Tuya',
187
+ mode: 'cloud',
188
+ options: undefined,
189
+ };
190
+
191
+ const noOptionsPlatform = new TuyaPlatform(mockLog, noOptionsConfig, mockAPI);
192
+ const isValid = noOptionsPlatform.validate();
193
+
194
+ expect(isValid).toBe(false);
195
+ });
196
+
197
+ test('requires local config for local mode', () => {
198
+ const noLocalConfig = {
199
+ platform: 'TuyaPlatform',
200
+ name: 'Tuya',
201
+ mode: 'local',
202
+ local: undefined,
203
+ };
204
+
205
+ const noLocalPlatform = new TuyaPlatform(mockLog, noLocalConfig, mockAPI);
206
+ const isValid = noLocalPlatform.validate();
207
+
208
+ expect(isValid).toBe(false);
209
+ });
210
+
211
+ test('validates cloud config with projectType 1', () => {
212
+ const cloudConfig = {
213
+ platform: 'TuyaPlatform',
214
+ name: 'Tuya',
215
+ mode: 'cloud',
216
+ options: {
217
+ projectType: '1',
218
+ endpoint: 'https://openapi.tuyaeu.com',
219
+ accessId: 'id',
220
+ accessKey: 'key',
221
+ username: 'user',
222
+ password: 'pass',
223
+ generateWeatherAccessory: false,
224
+ weatherAPI: '',
225
+ },
226
+ };
227
+
228
+ const cloudPlatform = new TuyaPlatform(mockLog, cloudConfig, mockAPI);
229
+ expect(cloudPlatform.options.projectType).toBe('1');
230
+ });
231
+
232
+ test('validates cloud config with projectType 2', () => {
233
+ const cloudConfig = {
234
+ platform: 'TuyaPlatform',
235
+ name: 'Tuya',
236
+ mode: 'cloud',
237
+ options: {
238
+ projectType: '2',
239
+ accessId: 'id',
240
+ accessKey: 'key',
241
+ countryCode: 1,
242
+ username: 'user',
243
+ password: 'pass',
244
+ appSchema: 'tuyaSmart',
245
+ generateWeatherAccessory: false,
246
+ weatherAPI: '',
247
+ },
248
+ };
249
+
250
+ const cloudPlatform = new TuyaPlatform(mockLog, cloudConfig, mockAPI);
251
+ expect(cloudPlatform.options.projectType).toBe('2');
252
+ });
253
+ });
254
+
255
+ describe('local only mode', () => {
256
+ test('does not require cloud options', () => {
257
+ const localConfig = {
258
+ platform: 'TuyaPlatform',
259
+ name: 'Tuya',
260
+ mode: 'local',
261
+ local: {
262
+ devices: [
263
+ {
264
+ tuyaDeviceId: 'device_1',
265
+ ip: '192.168.1.100',
266
+ tuyaKey: 'key123',
267
+ protocolVersion: '3.5',
268
+ },
269
+ ],
270
+ },
271
+ };
272
+
273
+ const localPlatform = new TuyaPlatform(mockLog, localConfig, mockAPI);
274
+ expect(localPlatform.mode).toBe('local');
275
+ });
276
+
277
+ test('requires local config block', () => {
278
+ const localNoConfigBlock = {
279
+ platform: 'TuyaPlatform',
280
+ name: 'Tuya',
281
+ mode: 'local',
282
+ };
283
+
284
+ const localPlatform = new TuyaPlatform(mockLog, localNoConfigBlock, mockAPI);
285
+ const isValid = localPlatform.validate();
286
+
287
+ expect(isValid).toBe(false);
288
+ });
289
+ });
290
+
291
+ describe('platform properties', () => {
292
+ test('stores platform config', () => {
293
+ expect(platform.platformConfig).toBeDefined();
294
+ expect(platform.platformConfig.platform).toBe('TuyaPlatform');
295
+ });
296
+
297
+ test('stores cloud options', () => {
298
+ expect(platform.options).toBeDefined();
299
+ expect(platform.options.projectType).toBe('2');
300
+ });
301
+
302
+ test('accesses credentials from config', () => {
303
+ expect(platform.options.accessId).toBe('test_id');
304
+ expect(platform.options.accessKey).toBe('test_key');
305
+ });
306
+ });
307
+
308
+ describe('accessory management', () => {
309
+ test('initializes empty accessory handlers', () => {
310
+ expect(platform.accessoryHandlers.length).toBe(0);
311
+ });
312
+
313
+ test('can add accessory handlers', () => {
314
+ const mockHandler = { displayName: 'Device 1' };
315
+ platform.accessoryHandlers.push(mockHandler as any);
316
+
317
+ expect(platform.accessoryHandlers.length).toBe(1);
318
+ });
319
+
320
+ test('can remove accessory handlers', () => {
321
+ const handler1 = { UUID: 'uuid-1' };
322
+ const handler2 = { UUID: 'uuid-2' };
323
+
324
+ platform.accessoryHandlers = [handler1 as any, handler2 as any];
325
+ platform.accessoryHandlers = platform.accessoryHandlers.filter((h) => (h as any).UUID !== 'uuid-1');
326
+
327
+ expect(platform.accessoryHandlers.length).toBe(1);
328
+ });
329
+
330
+ test('caches accessories', () => {
331
+ const mockAccessory = { UUID: 'uuid-1', displayName: 'Cached Device' };
332
+ platform.cachedAccessories.push(mockAccessory as any);
333
+
334
+ expect(platform.cachedAccessories.length).toBe(1);
335
+ });
336
+
337
+ test('can restore cached accessories', () => {
338
+ const mockAccessory1 = { UUID: 'uuid-1', displayName: 'Device 1' };
339
+ const mockAccessory2 = { UUID: 'uuid-2', displayName: 'Device 2' };
340
+
341
+ platform.cachedAccessories = [mockAccessory1 as any, mockAccessory2 as any];
342
+
343
+ expect(platform.cachedAccessories.length).toBe(2);
344
+ });
345
+ });
346
+
347
+ describe('device manager initialization', () => {
348
+ test('can initialize cloud device manager', () => {
349
+ platform.deviceManager = {} as any;
350
+ expect(platform.deviceManager).toBeDefined();
351
+ });
352
+
353
+ test('can initialize local device manager', () => {
354
+ platform.localDeviceManager = {} as any;
355
+ expect(platform.localDeviceManager).toBeDefined();
356
+ });
357
+
358
+ test('both managers can coexist in both mode', () => {
359
+ platform.deviceManager = {} as any;
360
+ platform.localDeviceManager = {} as any;
361
+
362
+ expect(platform.deviceManager).toBeDefined();
363
+ expect(platform.localDeviceManager).toBeDefined();
364
+ });
365
+ });
366
+
367
+ describe('both mode configuration', () => {
368
+ test('supports simultaneous cloud and local', () => {
369
+ const bothConfig = {
370
+ platform: 'TuyaPlatform',
371
+ name: 'Tuya',
372
+ mode: 'both',
373
+ options: {
374
+ projectType: '2',
375
+ accessId: 'id',
376
+ accessKey: 'key',
377
+ countryCode: 1,
378
+ username: 'user',
379
+ password: 'pass',
380
+ appSchema: 'tuyaSmart',
381
+ generateWeatherAccessory: false,
382
+ weatherAPI: '',
383
+ },
384
+ local: {
385
+ devices: [],
386
+ },
387
+ };
388
+
389
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
390
+ expect(bothPlatform.mode).toBe('both');
391
+ expect(bothPlatform.options).toBeDefined();
392
+ expect(bothPlatform.platformConfig.local).toBeDefined();
393
+ });
394
+
395
+ test('enriches local config with cloud device details', async () => {
396
+ const bothConfig = {
397
+ platform: 'TuyaPlatform',
398
+ name: 'Tuya',
399
+ mode: 'both',
400
+ options: {
401
+ projectType: '2',
402
+ accessId: 'id',
403
+ accessKey: 'key',
404
+ countryCode: 1,
405
+ username: 'user',
406
+ password: 'pass',
407
+ appSchema: 'tuyaSmart',
408
+ generateWeatherAccessory: false,
409
+ weatherAPI: '',
410
+ },
411
+ local: {
412
+ devices: [],
413
+ },
414
+ };
415
+
416
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
417
+
418
+ // Mock cloud device
419
+ const mockCloudDevice = {
420
+ id: 'test-device-123',
421
+ uuid: 'test-device-123',
422
+ name: 'Test Device',
423
+ product_id: 'product-123',
424
+ } as any;
425
+
426
+ // Mock the getDeviceDetails response
427
+ const mockDeviceDetails = {
428
+ success: true,
429
+ result: {
430
+ id: 'test-device-123',
431
+ local_key: 'test-local-key-abcdef',
432
+ ip: '192.168.1.100',
433
+ },
434
+ };
435
+
436
+ bothPlatform.deviceManager = {
437
+ getDeviceDetails: async () => mockDeviceDetails,
438
+ } as any;
439
+
440
+ // Enrich local config
441
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
442
+
443
+ // Verify local config was enriched
444
+ expect(bothPlatform.platformConfig.local).toBeDefined();
445
+ if (bothPlatform.platformConfig.local?.devices) {
446
+ expect(bothPlatform.platformConfig.local.devices.length).toBeGreaterThan(0);
447
+
448
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
449
+ expect(enrichedDevice.tuyaDeviceId).toBe('test-device-123');
450
+ expect(enrichedDevice.tuyaKey).toBe('test-local-key-abcdef');
451
+ expect(enrichedDevice.ip).toBe('192.168.1.100');
452
+ }
453
+ });
454
+
455
+ test('preserves manual local config when enriching', async () => {
456
+ const bothConfig = {
457
+ platform: 'TuyaPlatform',
458
+ name: 'Tuya',
459
+ mode: 'both',
460
+ options: {
461
+ projectType: '2',
462
+ accessId: 'id',
463
+ accessKey: 'key',
464
+ countryCode: 1,
465
+ username: 'user',
466
+ password: 'pass',
467
+ appSchema: 'tuyaSmart',
468
+ generateWeatherAccessory: false,
469
+ weatherAPI: '',
470
+ },
471
+ local: {
472
+ devices: [
473
+ {
474
+ tuyaDeviceId: 'manual-device-456',
475
+ tuyaKey: 'manual-key-xyz',
476
+ name: 'Manual Device',
477
+ },
478
+ ],
479
+ },
480
+ };
481
+
482
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
483
+
484
+ // Mock cloud device (different from manual one)
485
+ const mockCloudDevice = {
486
+ id: 'test-device-123',
487
+ uuid: 'test-device-123',
488
+ name: 'Test Device',
489
+ product_id: 'product-123',
490
+ } as any;
491
+
492
+ bothPlatform.deviceManager = {
493
+ getDeviceDetails: async () => ({
494
+ success: true,
495
+ result: {
496
+ id: 'test-device-123',
497
+ local_key: 'cloud-key-123',
498
+ ip: '192.168.1.100',
499
+ },
500
+ }),
501
+ } as any;
502
+
503
+ // Enrich local config
504
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
505
+
506
+ // Verify local config exists and has devices
507
+ expect(bothPlatform.platformConfig.local).toBeDefined();
508
+ if (bothPlatform.platformConfig.local?.devices) {
509
+ // Verify manual device is still there
510
+ const manualDevice = bothPlatform.platformConfig.local.devices.find(
511
+ d => d.tuyaDeviceId === 'manual-device-456'
512
+ );
513
+ expect(manualDevice).toBeDefined();
514
+ expect(manualDevice?.tuyaKey).toBe('manual-key-xyz');
515
+
516
+ // Verify new cloud device was added
517
+ const cloudDevice = bothPlatform.platformConfig.local.devices.find(
518
+ d => d.tuyaDeviceId === 'test-device-123'
519
+ );
520
+ expect(cloudDevice).toBeDefined();
521
+ expect(cloudDevice?.tuyaKey).toBe('cloud-key-123');
522
+ }
523
+ });
524
+
525
+ test('skips enrichment if cloud device details unavailable', async () => {
526
+ const bothConfig = {
527
+ platform: 'TuyaPlatform',
528
+ name: 'Tuya',
529
+ mode: 'both',
530
+ options: {
531
+ projectType: '2',
532
+ accessId: 'id',
533
+ accessKey: 'key',
534
+ countryCode: 1,
535
+ username: 'user',
536
+ password: 'pass',
537
+ appSchema: 'tuyaSmart',
538
+ generateWeatherAccessory: false,
539
+ weatherAPI: '',
540
+ },
541
+ local: {
542
+ devices: [],
543
+ },
544
+ };
545
+
546
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
547
+
548
+ const mockCloudDevice = {
549
+ id: 'test-device-123',
550
+ name: 'Test Device',
551
+ } as any;
552
+
553
+ bothPlatform.deviceManager = {
554
+ getDeviceDetails: async () => ({
555
+ success: false,
556
+ code: 'NOT_FOUND',
557
+ }),
558
+ } as any;
559
+
560
+ // Enrich local config
561
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
562
+
563
+ // Verify nothing was added (since API failed)
564
+ expect(bothPlatform.platformConfig.local).toBeDefined();
565
+ if (bothPlatform.platformConfig.local?.devices) {
566
+ expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
567
+ }
568
+ });
569
+ });
570
+
571
+ describe('homebridge lifecycle', () => {
572
+ test('has configureAccessory method', () => {
573
+ expect(typeof platform.configureAccessory).toBe('function');
574
+ });
575
+
576
+ test('has initialize method', () => {
577
+ expect(platform).toBeDefined();
578
+ expect(platform.config).toBeDefined();
579
+ });
580
+ });
581
+
582
+ describe('platform identification', () => {
583
+ test('platform name is set', () => {
584
+ expect(mockConfig.platform).toBe('TuyaPlatform');
585
+ });
586
+
587
+ test('plugin name is accessible', () => {
588
+ expect(mockConfig.name).toBe('Tuya');
589
+ });
590
+ });
591
+
592
+ describe('device type support', () => {
593
+ test('supports light devices', () => {
594
+ // Lights are category 'dj'
595
+ expect(mockConfig).toBeDefined();
596
+ });
597
+
598
+ test('supports switch devices', () => {
599
+ // Switches are category 'kg'
600
+ expect(mockConfig).toBeDefined();
601
+ });
602
+
603
+ test('supports multiple device types simultaneously', () => {
604
+ expect(mockConfig).toBeDefined();
605
+ });
606
+ });
607
+
608
+ describe('error handling', () => {
609
+ test('logs errors to homebridge', () => {
610
+ platform.log.error('Test error');
611
+ expect(mockLog.error).toHaveBeenCalledWith('Test error');
612
+ });
613
+
614
+ test('validates configuration before use', () => {
615
+ const invalidConfig = {
616
+ platform: 'TuyaPlatform',
617
+ name: 'Tuya',
618
+ };
619
+
620
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
621
+ const isValid = invalidPlatform.validate();
622
+
623
+ expect(typeof isValid).toBe('boolean');
624
+ });
625
+ });
626
+
627
+ describe('configuration edge cases', () => {
628
+ test('handles missing project type', () => {
629
+ const noProjectTypeConfig = {
630
+ platform: 'TuyaPlatform',
631
+ name: 'Tuya',
632
+ mode: 'cloud',
633
+ options: {
634
+ accessId: 'id',
635
+ accessKey: 'key',
636
+ },
637
+ };
638
+
639
+ const noProjectPlatform = new TuyaPlatform(mockLog, noProjectTypeConfig, mockAPI);
640
+ expect(noProjectPlatform).toBeDefined();
641
+ });
642
+
643
+ test('handles empty local devices array', () => {
644
+ const emptyDevicesConfig = {
645
+ platform: 'TuyaPlatform',
646
+ name: 'Tuya',
647
+ mode: 'local',
648
+ local: {
649
+ devices: [],
650
+ },
651
+ };
652
+
653
+ const emptyPlatform = new TuyaPlatform(mockLog, emptyDevicesConfig, mockAPI);
654
+ expect(emptyPlatform.mode).toBe('local');
655
+ });
656
+
657
+ test('handles weather accessory option', () => {
658
+ const weatherConfig = {
659
+ ...mockConfig,
660
+ options: {
661
+ ...mockConfig.options,
662
+ generateWeatherAccessory: true,
663
+ weatherAPI: 'openweathermap',
664
+ },
665
+ };
666
+
667
+ const weatherPlatform = new TuyaPlatform(mockLog, weatherConfig, mockAPI);
668
+ expect((weatherPlatform.options as any).generateWeatherAccessory).toBe(true);
669
+ });
670
+ });
671
+
672
+ describe('multiple platform instances', () => {
673
+ test('maintain separate state', () => {
674
+ const config1 = {
675
+ ...mockConfig,
676
+ name: 'Tuya1',
677
+ };
678
+
679
+ const config2 = {
680
+ ...mockConfig,
681
+ name: 'Tuya2',
682
+ };
683
+
684
+ const platform1 = new TuyaPlatform(mockLog, config1, mockAPI);
685
+ const platform2 = new TuyaPlatform(mockLog, config2, mockAPI);
686
+
687
+ platform1.cachedAccessories.push({} as any);
688
+
689
+ expect(platform1.cachedAccessories.length).toBe(1);
690
+ expect(platform2.cachedAccessories.length).toBe(0);
691
+ });
692
+ });
693
+
694
+ describe('API field variations and error handling', () => {
695
+ test('handles localKey instead of local_key in API response', async () => {
696
+ const bothConfig = {
697
+ platform: 'TuyaPlatform',
698
+ name: 'Tuya',
699
+ mode: 'both',
700
+ options: {
701
+ projectType: '2',
702
+ accessId: 'id',
703
+ accessKey: 'key',
704
+ countryCode: 1,
705
+ username: 'user',
706
+ password: 'pass',
707
+ appSchema: 'tuyaSmart',
708
+ generateWeatherAccessory: false,
709
+ weatherAPI: '',
710
+ },
711
+ local: {
712
+ devices: [],
713
+ },
714
+ };
715
+
716
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
717
+
718
+ const mockCloudDevice = {
719
+ id: 'test-device-123',
720
+ uuid: 'test-device-123',
721
+ name: 'Test Device',
722
+ product_id: 'product-123',
723
+ } as any;
724
+
725
+ // Response with localKey (camelCase) instead of local_key (snake_case)
726
+ bothPlatform.deviceManager = {
727
+ getDeviceDetails: async () => ({
728
+ success: true,
729
+ result: {
730
+ id: 'test-device-123',
731
+ localKey: 'alternate-format-key', // camelCase variant
732
+ ip: '192.168.1.100',
733
+ },
734
+ }),
735
+ } as any;
736
+
737
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
738
+
739
+ if (bothPlatform.platformConfig.local?.devices) {
740
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
741
+ expect(enrichedDevice.tuyaKey).toBe('alternate-format-key');
742
+ }
743
+ });
744
+
745
+ test('handles address field instead of ip in API response', async () => {
746
+ const bothConfig = {
747
+ platform: 'TuyaPlatform',
748
+ name: 'Tuya',
749
+ mode: 'both',
750
+ options: {
751
+ projectType: '2',
752
+ accessId: 'id',
753
+ accessKey: 'key',
754
+ countryCode: 1,
755
+ username: 'user',
756
+ password: 'pass',
757
+ appSchema: 'tuyaSmart',
758
+ generateWeatherAccessory: false,
759
+ weatherAPI: '',
760
+ },
761
+ local: {
762
+ devices: [],
763
+ },
764
+ };
765
+
766
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
767
+
768
+ const mockCloudDevice = {
769
+ id: 'test-device-456',
770
+ name: 'Another Device',
771
+ } as any;
772
+
773
+ bothPlatform.deviceManager = {
774
+ getDeviceDetails: async () => ({
775
+ success: true,
776
+ result: {
777
+ id: 'test-device-456',
778
+ local_key: 'test-key-456',
779
+ address: '10.0.0.50', // Uses 'address' instead of 'ip'
780
+ },
781
+ }),
782
+ } as any;
783
+
784
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
785
+
786
+ if (bothPlatform.platformConfig.local?.devices) {
787
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
788
+ expect(enrichedDevice.ip).toBe('10.0.0.50');
789
+ }
790
+ });
791
+
792
+ test('uses uuid fallback when cloud device id is missing', async () => {
793
+ const bothConfig = {
794
+ platform: 'TuyaPlatform',
795
+ name: 'Tuya',
796
+ mode: 'both',
797
+ options: {
798
+ projectType: '2',
799
+ accessId: 'id',
800
+ accessKey: 'key',
801
+ countryCode: 1,
802
+ username: 'user',
803
+ password: 'pass',
804
+ appSchema: 'tuyaSmart',
805
+ generateWeatherAccessory: false,
806
+ weatherAPI: '',
807
+ },
808
+ local: {
809
+ devices: [],
810
+ },
811
+ };
812
+
813
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
814
+
815
+ const mockCloudDevice = {
816
+ uuid: 'fallback-device-123',
817
+ name: 'Fallback Device',
818
+ } as any;
819
+
820
+ bothPlatform.deviceManager = {
821
+ getDeviceDetails: async () => ({
822
+ success: true,
823
+ result: {
824
+ uuid: 'fallback-device-123',
825
+ local_key: 'fallback-key-123',
826
+ ip: '192.168.1.101',
827
+ },
828
+ }),
829
+ } as any;
830
+
831
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
832
+
833
+ if (bothPlatform.platformConfig.local?.devices) {
834
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
835
+ expect(enrichedDevice.tuyaDeviceId).toBe('fallback-device-123');
836
+ expect(enrichedDevice.tuyaKey).toBe('fallback-key-123');
837
+ }
838
+ });
839
+
840
+ test('propagates cloud category into local config', async () => {
841
+ const bothConfig = {
842
+ platform: 'TuyaPlatform',
843
+ name: 'Tuya',
844
+ mode: 'both',
845
+ options: {
846
+ projectType: '2',
847
+ accessId: 'id',
848
+ accessKey: 'key',
849
+ countryCode: 1,
850
+ username: 'user',
851
+ password: 'pass',
852
+ appSchema: 'tuyaSmart',
853
+ generateWeatherAccessory: false,
854
+ weatherAPI: '',
855
+ },
856
+ local: {
857
+ devices: [],
858
+ },
859
+ };
860
+
861
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
862
+
863
+ const mockCloudDevice = {
864
+ id: 'category-device-123',
865
+ name: 'Category Device',
866
+ category: 'kg',
867
+ product_id: 'abc123',
868
+ } as any;
869
+
870
+ bothPlatform.deviceManager = {
871
+ getDeviceDetails: async () => ({
872
+ success: true,
873
+ result: {
874
+ id: 'category-device-123',
875
+ local_key: 'category-key-123',
876
+ ip: '192.168.1.102',
877
+ },
878
+ }),
879
+ } as any;
880
+
881
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
882
+
883
+ if (bothPlatform.platformConfig.local?.devices) {
884
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
885
+ expect(enrichedDevice.category).toBe('kg');
886
+ }
887
+ });
888
+
889
+ test('handles missing local_key gracefully', async () => {
890
+ const bothConfig = {
891
+ platform: 'TuyaPlatform',
892
+ name: 'Tuya',
893
+ mode: 'both',
894
+ options: {
895
+ projectType: '2',
896
+ accessId: 'id',
897
+ accessKey: 'key',
898
+ countryCode: 1,
899
+ username: 'user',
900
+ password: 'pass',
901
+ appSchema: 'tuyaSmart',
902
+ generateWeatherAccessory: false,
903
+ weatherAPI: '',
904
+ },
905
+ local: {
906
+ devices: [],
907
+ },
908
+ };
909
+
910
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
911
+
912
+ const mockCloudDevice = {
913
+ id: 'test-device-no-key',
914
+ name: 'Device Without Local Key',
915
+ } as any;
916
+
917
+ bothPlatform.deviceManager = {
918
+ getDeviceDetails: async () => ({
919
+ success: true,
920
+ result: {
921
+ id: 'test-device-no-key',
922
+ // No local_key or localKey provided
923
+ ip: '192.168.1.200',
924
+ },
925
+ }),
926
+ } as any;
927
+
928
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
929
+
930
+ // Device should not be added to local config without local_key
931
+ if (bothPlatform.platformConfig.local?.devices) {
932
+ expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
933
+ }
934
+ });
935
+
936
+ test('handles API timeout with grace', async () => {
937
+ const bothConfig = {
938
+ platform: 'TuyaPlatform',
939
+ name: 'Tuya',
940
+ mode: 'both',
941
+ options: {
942
+ projectType: '2',
943
+ accessId: 'id',
944
+ accessKey: 'key',
945
+ countryCode: 1,
946
+ username: 'user',
947
+ password: 'pass',
948
+ appSchema: 'tuyaSmart',
949
+ generateWeatherAccessory: false,
950
+ weatherAPI: '',
951
+ },
952
+ local: {
953
+ devices: [],
954
+ },
955
+ };
956
+
957
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
958
+
959
+ const mockCloudDevice = {
960
+ id: 'test-device-timeout',
961
+ name: 'Timeout Device',
962
+ } as any;
963
+
964
+ bothPlatform.deviceManager = {
965
+ getDeviceDetails: async () => {
966
+ throw new Error('Request timeout');
967
+ },
968
+ } as any;
969
+
970
+ // Should not throw, but handle gracefully
971
+ await expect(bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice])).resolves.toBeUndefined();
972
+
973
+ // Local devices should remain empty (timeout treated as error)
974
+ if (bothPlatform.platformConfig.local?.devices) {
975
+ expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
976
+ }
977
+ });
978
+ });
979
+
980
+ describe('mixed config scenarios', () => {
981
+ test('updates manual local config with cloud-provided local_key', async () => {
982
+ const bothConfig = {
983
+ platform: 'TuyaPlatform',
984
+ name: 'Tuya',
985
+ mode: 'both',
986
+ options: {
987
+ projectType: '2',
988
+ accessId: 'id',
989
+ accessKey: 'key',
990
+ countryCode: 1,
991
+ username: 'user',
992
+ password: 'pass',
993
+ appSchema: 'tuyaSmart',
994
+ generateWeatherAccessory: false,
995
+ weatherAPI: '',
996
+ },
997
+ local: {
998
+ devices: [
999
+ {
1000
+ tuyaDeviceId: 'manual-device-xyz',
1001
+ // User provided device ID but no key
1002
+ name: 'Manual Device No Key',
1003
+ },
1004
+ ],
1005
+ },
1006
+ };
1007
+
1008
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
1009
+
1010
+ const mockCloudDevice = {
1011
+ id: 'manual-device-xyz',
1012
+ name: 'Manual Device No Key',
1013
+ } as any;
1014
+
1015
+ bothPlatform.deviceManager = {
1016
+ getDeviceDetails: async () => ({
1017
+ success: true,
1018
+ result: {
1019
+ id: 'manual-device-xyz',
1020
+ local_key: 'cloud-filled-key',
1021
+ ip: '192.168.1.99',
1022
+ },
1023
+ }),
1024
+ } as any;
1025
+
1026
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
1027
+
1028
+ if (bothPlatform.platformConfig.local?.devices) {
1029
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
1030
+ expect(enrichedDevice.tuyaDeviceId).toBe('manual-device-xyz');
1031
+ expect(enrichedDevice.tuyaKey).toBe('cloud-filled-key');
1032
+ expect(enrichedDevice.ip).toBe('192.168.1.99');
1033
+ // Original name should be preserved
1034
+ expect(enrichedDevice.name).toBe('Manual Device No Key');
1035
+ }
1036
+ });
1037
+
1038
+ test('does not overwrite existing manual local_key', async () => {
1039
+ const bothConfig = {
1040
+ platform: 'TuyaPlatform',
1041
+ name: 'Tuya',
1042
+ mode: 'both',
1043
+ options: {
1044
+ projectType: '2',
1045
+ accessId: 'id',
1046
+ accessKey: 'key',
1047
+ countryCode: 1,
1048
+ username: 'user',
1049
+ password: 'pass',
1050
+ appSchema: 'tuyaSmart',
1051
+ generateWeatherAccessory: false,
1052
+ weatherAPI: '',
1053
+ },
1054
+ local: {
1055
+ devices: [
1056
+ {
1057
+ tuyaDeviceId: 'device-with-manual-key',
1058
+ tuyaKey: 'user-provided-key-123', // User explicitly set this
1059
+ name: 'Device With Manual Key',
1060
+ },
1061
+ ],
1062
+ },
1063
+ };
1064
+
1065
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
1066
+
1067
+ const mockCloudDevice = {
1068
+ id: 'device-with-manual-key',
1069
+ name: 'Device With Manual Key',
1070
+ } as any;
1071
+
1072
+ bothPlatform.deviceManager = {
1073
+ getDeviceDetails: async () => ({
1074
+ success: true,
1075
+ result: {
1076
+ id: 'device-with-manual-key',
1077
+ local_key: 'different-cloud-key',
1078
+ ip: '192.168.1.88',
1079
+ },
1080
+ }),
1081
+ } as any;
1082
+
1083
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
1084
+
1085
+ if (bothPlatform.platformConfig.local?.devices) {
1086
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
1087
+ // Manual key should be skipped (device has tuyaKey already)
1088
+ expect(enrichedDevice.tuyaKey).toBe('user-provided-key-123');
1089
+ }
1090
+ });
1091
+ });
1092
+
1093
+ describe('multi-device enrichment', () => {
1094
+ test('enriches multiple cloud devices in parallel', async () => {
1095
+ const bothConfig = {
1096
+ platform: 'TuyaPlatform',
1097
+ name: 'Tuya',
1098
+ mode: 'both',
1099
+ options: {
1100
+ projectType: '2',
1101
+ accessId: 'id',
1102
+ accessKey: 'key',
1103
+ countryCode: 1,
1104
+ username: 'user',
1105
+ password: 'pass',
1106
+ appSchema: 'tuyaSmart',
1107
+ generateWeatherAccessory: false,
1108
+ weatherAPI: '',
1109
+ },
1110
+ local: {
1111
+ devices: [],
1112
+ },
1113
+ };
1114
+
1115
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
1116
+
1117
+ const mockCloudDevices = [
1118
+ { id: 'device-1', uuid: 'device-1', name: 'Device 1', product_id: 'prod-1' },
1119
+ { id: 'device-2', uuid: 'device-2', name: 'Device 2', product_id: 'prod-2' },
1120
+ { id: 'device-3', uuid: 'device-3', name: 'Device 3', product_id: 'prod-3' },
1121
+ ] as any;
1122
+
1123
+ const callCount: Record<string, number> = {};
1124
+ bothPlatform.deviceManager = {
1125
+ getDeviceDetails: async (deviceId: string) => {
1126
+ callCount[deviceId] = (callCount[deviceId] || 0) + 1;
1127
+ return {
1128
+ success: true,
1129
+ result: {
1130
+ id: deviceId,
1131
+ local_key: `key-${deviceId}`,
1132
+ ip: `192.168.1.${Math.floor(Math.random() * 254) + 1}`,
1133
+ },
1134
+ };
1135
+ },
1136
+ } as any;
1137
+
1138
+ await bothPlatform.enrichLocalConfigFromCloud(mockCloudDevices);
1139
+
1140
+ if (bothPlatform.platformConfig.local?.devices) {
1141
+ expect(bothPlatform.platformConfig.local.devices.length).toBe(3);
1142
+
1143
+ const device1 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-1');
1144
+ const device2 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-2');
1145
+ const device3 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-3');
1146
+
1147
+ expect(device1?.tuyaKey).toBe('key-device-1');
1148
+ expect(device2?.tuyaKey).toBe('key-device-2');
1149
+ expect(device3?.tuyaKey).toBe('key-device-3');
1150
+ }
1151
+ });
1152
+
1153
+ test('continues enrichment if single device fails', async () => {
1154
+ const bothConfig = {
1155
+ platform: 'TuyaPlatform',
1156
+ name: 'Tuya',
1157
+ mode: 'both',
1158
+ options: {
1159
+ projectType: '2',
1160
+ accessId: 'id',
1161
+ accessKey: 'key',
1162
+ countryCode: 1,
1163
+ username: 'user',
1164
+ password: 'pass',
1165
+ appSchema: 'tuyaSmart',
1166
+ generateWeatherAccessory: false,
1167
+ weatherAPI: '',
1168
+ },
1169
+ local: {
1170
+ devices: [],
1171
+ },
1172
+ };
1173
+
1174
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
1175
+
1176
+ const mockCloudDevices = [
1177
+ { id: 'device-ok-1', uuid: 'device-ok-1', name: 'OK Device 1' },
1178
+ { id: 'device-fail', uuid: 'device-fail', name: 'Failing Device' },
1179
+ { id: 'device-ok-2', uuid: 'device-ok-2', name: 'OK Device 2' },
1180
+ ] as any;
1181
+
1182
+ bothPlatform.deviceManager = {
1183
+ getDeviceDetails: async (deviceId: string) => {
1184
+ if (deviceId === 'device-fail') {
1185
+ return { success: false, code: 'ERROR' };
1186
+ }
1187
+ return {
1188
+ success: true,
1189
+ result: {
1190
+ id: deviceId,
1191
+ local_key: `key-${deviceId}`,
1192
+ ip: '192.168.1.100',
1193
+ },
1194
+ };
1195
+ },
1196
+ } as any;
1197
+
1198
+ await bothPlatform.enrichLocalConfigFromCloud(mockCloudDevices);
1199
+
1200
+ if (bothPlatform.platformConfig.local?.devices) {
1201
+ // Should have 2 devices (one failed, skipped)
1202
+ expect(bothPlatform.platformConfig.local.devices.length).toBe(2);
1203
+
1204
+ const hasOk1 = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-ok-1');
1205
+ const hasOk2 = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-ok-2');
1206
+ const hasFailed = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-fail');
1207
+
1208
+ expect(hasOk1).toBe(true);
1209
+ expect(hasOk2).toBe(true);
1210
+ expect(hasFailed).toBe(false);
1211
+ }
1212
+ });
1213
+ });
1214
+
1215
+ describe('device ID consistency and deduplication', () => {
1216
+ test('uses consistent UUID for same device across cloud and local', () => {
1217
+ const bothConfig = {
1218
+ platform: 'TuyaPlatform',
1219
+ name: 'Tuya',
1220
+ mode: 'both',
1221
+ options: {
1222
+ projectType: '2',
1223
+ accessId: 'id',
1224
+ accessKey: 'key',
1225
+ countryCode: 1,
1226
+ username: 'user',
1227
+ password: 'pass',
1228
+ appSchema: 'tuyaSmart',
1229
+ generateWeatherAccessory: false,
1230
+ weatherAPI: '',
1231
+ },
1232
+ local: {
1233
+ devices: [],
1234
+ },
1235
+ };
1236
+
1237
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
1238
+
1239
+ // Create mock cloud and local devices with same ID
1240
+ const mockCloudDevice = {
1241
+ id: 'same-device-id',
1242
+ uuid: 'same-device-id',
1243
+ name: 'Test Device (Cloud)',
1244
+ } as any;
1245
+
1246
+ const mockLocalDevice = {
1247
+ id: 'same-device-id',
1248
+ uuid: 'same-device-id',
1249
+ name: 'Test Device (Local)',
1250
+ } as any;
1251
+
1252
+ // Both should map to same UUID
1253
+ const cloudUUID = bothPlatform.api.hap.uuid.generate(mockCloudDevice.id);
1254
+ const localUUID = bothPlatform.api.hap.uuid.generate(mockLocalDevice.id);
1255
+
1256
+ expect(cloudUUID).toBe(localUUID);
1257
+ });
1258
+
1259
+ test('enrichment preserves device ID for accessory mapping', async () => {
1260
+ const bothConfig = {
1261
+ platform: 'TuyaPlatform',
1262
+ name: 'Tuya',
1263
+ mode: 'both',
1264
+ options: {
1265
+ projectType: '2',
1266
+ accessId: 'id',
1267
+ accessKey: 'key',
1268
+ countryCode: 1,
1269
+ username: 'user',
1270
+ password: 'pass',
1271
+ appSchema: 'tuyaSmart',
1272
+ generateWeatherAccessory: false,
1273
+ weatherAPI: '',
1274
+ },
1275
+ local: {
1276
+ devices: [],
1277
+ },
1278
+ };
1279
+
1280
+ const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
1281
+
1282
+ const mockCloudDevice = {
1283
+ id: 'unique-device-id',
1284
+ uuid: 'unique-device-id',
1285
+ name: 'Device',
1286
+ product_id: 'product-123',
1287
+ } as any;
1288
+
1289
+ bothPlatform.deviceManager = {
1290
+ getDeviceDetails: async () => ({
1291
+ success: true,
1292
+ result: {
1293
+ id: 'unique-device-id',
1294
+ local_key: 'test-key',
1295
+ ip: '192.168.1.1',
1296
+ },
1297
+ }),
1298
+ } as any;
1299
+
1300
+ await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
1301
+
1302
+ if (bothPlatform.platformConfig.local?.devices) {
1303
+ const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
1304
+ // Device ID should be preserved for accessory mapping
1305
+ expect(enrichedDevice.tuyaDeviceId).toBe('unique-device-id');
1306
+ }
1307
+ });
1308
+ });
1309
+ });
1310
+