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

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 (298) hide show
  1. package/README.md +354 -142
  2. package/config.schema.json +1221 -893
  3. package/dist/cloud/api/TuyaOpenAPI.js +108 -60
  4. package/dist/cloud/api/TuyaOpenAPI.js.map +1 -1
  5. package/dist/cloud/api/TuyaOpenMQ.js +45 -22
  6. package/dist/cloud/api/TuyaOpenMQ.js.map +1 -1
  7. package/dist/cloud/device/{TuyaDeviceManager.js → TuyaCloudDeviceManager.js} +146 -96
  8. package/dist/cloud/device/TuyaCloudDeviceManager.js.map +1 -0
  9. package/dist/cloud/device/TuyaCustomDeviceManager.js +75 -5
  10. package/dist/cloud/device/TuyaCustomDeviceManager.js.map +1 -1
  11. package/dist/cloud/device/TuyaDevice.js.map +1 -1
  12. package/dist/cloud/device/TuyaHomeDeviceManager.js +81 -13
  13. package/dist/cloud/device/TuyaHomeDeviceManager.js.map +1 -1
  14. package/dist/config.js +26 -5
  15. package/dist/config.js.map +1 -1
  16. package/dist/local/DynamicChildDiscovery.js +2 -1
  17. package/dist/local/DynamicChildDiscovery.js.map +1 -1
  18. package/dist/local/LocalDevice.js +16 -3
  19. package/dist/local/LocalDevice.js.map +1 -1
  20. package/dist/local/LocalDeviceManager.js +361 -164
  21. package/dist/local/LocalDeviceManager.js.map +1 -1
  22. package/dist/local/TuyaDiscovery.js +16 -7
  23. package/dist/local/TuyaDiscovery.js.map +1 -1
  24. package/dist/local/ZigbeeGatewayDetection.js +7 -4
  25. package/dist/local/ZigbeeGatewayDetection.js.map +1 -1
  26. package/dist/local/protocol/ProtocolUtilities.js +34 -17
  27. package/dist/local/protocol/ProtocolUtilities.js.map +1 -1
  28. package/dist/local/protocol/ProtocolV31V32.js +4 -2
  29. package/dist/local/protocol/ProtocolV31V32.js.map +1 -1
  30. package/dist/local/protocol/ProtocolV33.js +4 -2
  31. package/dist/local/protocol/ProtocolV33.js.map +1 -1
  32. package/dist/local/protocol/ProtocolV34.js +10 -5
  33. package/dist/local/protocol/ProtocolV34.js.map +1 -1
  34. package/dist/local/protocol/ProtocolV35.js +8 -4
  35. package/dist/local/protocol/ProtocolV35.js.map +1 -1
  36. package/dist/platform.js +253 -493
  37. package/dist/platform.js.map +1 -1
  38. package/dist/settings.js +0 -2
  39. package/dist/settings.js.map +1 -1
  40. package/dist/shared/TuyaDeviceManager.js +147 -0
  41. package/dist/shared/TuyaDeviceManager.js.map +1 -0
  42. package/dist/shared/TuyaHybridDeviceManager.js +215 -0
  43. package/dist/shared/TuyaHybridDeviceManager.js.map +1 -0
  44. package/dist/shared/accessory/AccessoryFactory.js +350 -0
  45. package/dist/shared/accessory/AccessoryFactory.js.map +1 -0
  46. package/dist/shared/{accessories → accessory}/AirConditionerAccessory.js +21 -15
  47. package/dist/shared/accessory/AirConditionerAccessory.js.map +1 -0
  48. package/dist/shared/{accessories → accessory}/AirPurifierAccessory.js.map +1 -1
  49. package/dist/shared/accessory/AirQualitySensorAccessory.js.map +1 -0
  50. package/dist/shared/{accessories → accessory}/BaseAccessory.js +16 -95
  51. package/dist/shared/accessory/BaseAccessory.js.map +1 -0
  52. package/dist/shared/{accessories → accessory}/BlindsAccessory.js.map +1 -1
  53. package/dist/shared/{accessories → accessory}/CameraAccessory.js +2 -2
  54. package/dist/shared/accessory/CameraAccessory.js.map +1 -0
  55. package/dist/shared/{accessories → accessory}/CarbonDioxideSensorAccessory.js.map +1 -1
  56. package/dist/shared/{accessories → accessory}/CarbonMonoxideSensorAccessory.js.map +1 -1
  57. package/dist/shared/{accessories → accessory}/ContactSensorAccessory.js.map +1 -1
  58. package/dist/shared/{accessories → accessory}/DehumidifierAccessory.js.map +1 -1
  59. package/dist/shared/{accessories → accessory}/DiffuserAccessory.js.map +1 -1
  60. package/dist/shared/{accessories → accessory}/DimmerAccessory.js.map +1 -1
  61. package/dist/shared/{accessories → accessory}/DoorbellAccessory.js.map +1 -1
  62. package/dist/shared/{accessories → accessory}/ExtractionHoodAccessory.js.map +1 -1
  63. package/dist/shared/{accessories → accessory}/FanAccessory.js.map +1 -1
  64. package/dist/shared/{accessories → accessory}/GarageDoorAccessory.js +19 -2
  65. package/dist/shared/accessory/GarageDoorAccessory.js.map +1 -0
  66. package/dist/shared/{accessories → accessory}/HeaterAccessory.js.map +1 -1
  67. package/dist/shared/{accessories → accessory}/HeaterAccessory_old.js.map +1 -1
  68. package/dist/shared/accessory/HumanPresenceSensorAccessory.js.map +1 -0
  69. package/dist/shared/{accessories → accessory}/HumidifierAccessory.js.map +1 -1
  70. package/dist/shared/accessory/IRAdapter.js +167 -0
  71. package/dist/shared/accessory/IRAdapter.js.map +1 -0
  72. package/dist/shared/accessory/IRAirConditionerAccessory.js.map +1 -0
  73. package/dist/shared/accessory/IRControlHubAccessory.js.map +1 -0
  74. package/dist/shared/accessory/IRControlHubSubAccessory.js.map +1 -0
  75. package/dist/shared/accessory/IRFanAccessory.js +90 -0
  76. package/dist/shared/accessory/IRFanAccessory.js.map +1 -0
  77. package/dist/shared/accessory/IRGenericAccessory.js.map +1 -0
  78. package/dist/shared/{accessories → accessory}/LeakSensorAccessory.js.map +1 -1
  79. package/dist/shared/{accessories → accessory}/LightAccessory.js.map +1 -1
  80. package/dist/shared/{accessories → accessory}/LightSensorAccessory.js.map +1 -1
  81. package/dist/shared/{accessories → accessory}/LocationWeatherAccessory.js +1 -1
  82. package/dist/shared/accessory/LocationWeatherAccessory.js.map +1 -0
  83. package/dist/shared/{accessories → accessory}/LockAccessory.js +3 -3
  84. package/dist/shared/{accessories → accessory}/LockAccessory.js.map +1 -1
  85. package/dist/shared/accessory/MotionSensorAccessory.js.map +1 -0
  86. package/dist/shared/accessory/OutletAccessory.js.map +1 -0
  87. package/dist/shared/{accessories → accessory}/PetFeederAccessory.js.map +1 -1
  88. package/dist/shared/{accessories → accessory}/SceneAccessory.js +1 -1
  89. package/dist/shared/accessory/SceneAccessory.js.map +1 -0
  90. package/dist/shared/accessory/SceneSwitchAccessory.js.map +1 -0
  91. package/dist/shared/accessory/SecuritySystemAccessory.js.map +1 -0
  92. package/dist/shared/{accessories → accessory}/SmokeSensorAccessory.js.map +1 -1
  93. package/dist/shared/{accessories → accessory}/SwitchAccessory.js +1 -45
  94. package/dist/shared/accessory/SwitchAccessory.js.map +1 -0
  95. package/dist/shared/accessory/TemperatureHumiditySensorAccessory.js.map +1 -0
  96. package/dist/shared/{accessories → accessory}/ThermostatAccessory.js.map +1 -1
  97. package/dist/shared/{accessories → accessory}/TowerRackAccessory.js.map +1 -1
  98. package/dist/shared/{accessories → accessory}/ValveAccessory.js.map +1 -1
  99. package/dist/shared/{accessories → accessory}/VibrationSensorAccessory.js.map +1 -1
  100. package/dist/shared/accessory/WeatherStationAccessory.js.map +1 -0
  101. package/dist/shared/accessory/WetBulbGlobeTemperatureAccessory.js.map +1 -0
  102. package/dist/shared/{accessories → accessory}/WhiteNoiseLightAccessory.js +1 -0
  103. package/dist/shared/accessory/WhiteNoiseLightAccessory.js.map +1 -0
  104. package/dist/shared/accessory/WindowAccessory.js.map +1 -0
  105. package/dist/shared/{accessories → accessory}/WindowCoveringAccessory.js.map +1 -1
  106. package/dist/shared/{accessories → accessory}/WirelessSwitchAccessory.js.map +1 -1
  107. package/dist/shared/accessory/characteristic/Active.js.map +1 -0
  108. package/dist/shared/accessory/characteristic/AirQuality.js.map +1 -0
  109. package/dist/shared/{accessories → accessory}/characteristic/CurrentRelativeHumidity.js +3 -0
  110. package/dist/shared/accessory/characteristic/CurrentRelativeHumidity.js.map +1 -0
  111. package/dist/shared/accessory/characteristic/CurrentTemperature.js.map +1 -0
  112. package/dist/shared/accessory/characteristic/CurrentWeather.js.map +1 -0
  113. package/dist/shared/accessory/characteristic/CurrentWeatherByOpenMeteo.js.map +1 -0
  114. package/dist/shared/{accessories → accessory}/characteristic/CurrentWetBulbGlobeTemperature.js +3 -2
  115. package/dist/shared/accessory/characteristic/CurrentWetBulbGlobeTemperature.js.map +1 -0
  116. package/dist/shared/{accessories → accessory}/characteristic/EnergyUsage.js +0 -5
  117. package/dist/shared/accessory/characteristic/EnergyUsage.js.map +1 -0
  118. package/dist/shared/{accessories → accessory}/characteristic/Light.js +28 -5
  119. package/dist/shared/accessory/characteristic/Light.js.map +1 -0
  120. package/dist/shared/accessory/characteristic/LightSensor.js.map +1 -0
  121. package/dist/shared/{accessories → accessory}/characteristic/LockPhysicalControls.js +3 -0
  122. package/dist/shared/accessory/characteristic/LockPhysicalControls.js.map +1 -0
  123. package/dist/shared/accessory/characteristic/MotionDetected.js.map +1 -0
  124. package/dist/shared/accessory/characteristic/Name.js.map +1 -0
  125. package/dist/shared/accessory/characteristic/OccupancyDetected.js.map +1 -0
  126. package/dist/shared/{accessories → accessory}/characteristic/On.js +1 -0
  127. package/dist/shared/accessory/characteristic/On.js.map +1 -0
  128. package/dist/shared/accessory/characteristic/OutletInUse.js.map +1 -0
  129. package/dist/shared/accessory/characteristic/ProgrammableSwitchEvent.js.map +1 -0
  130. package/dist/shared/{accessories → accessory}/characteristic/RelativeHumidityDehumidifierThreshold.js +3 -0
  131. package/dist/shared/accessory/characteristic/RelativeHumidityDehumidifierThreshold.js.map +1 -0
  132. package/dist/shared/accessory/characteristic/RotationSpeed.js.map +1 -0
  133. package/dist/shared/accessory/characteristic/SecuritySystemState.js.map +1 -0
  134. package/dist/shared/{accessories → accessory}/characteristic/SwingMode.js +3 -0
  135. package/dist/shared/accessory/characteristic/SwingMode.js.map +1 -0
  136. package/dist/shared/accessory/characteristic/TargetTemperature.js.map +1 -0
  137. package/dist/shared/accessory/characteristic/TemperatureDisplayUnits.js.map +1 -0
  138. package/dist/shared/util/ConfigHash.js +2 -2
  139. package/dist/shared/util/ConfigHash.js.map +1 -1
  140. package/dist/shared/util/ConfigMigrator.js +222 -0
  141. package/dist/shared/util/ConfigMigrator.js.map +1 -0
  142. package/dist/shared/util/FfmpegStreamingProcess.js +62 -0
  143. package/dist/shared/util/FfmpegStreamingProcess.js.map +1 -1
  144. package/dist/shared/util/InfraredTool.js +0 -2
  145. package/dist/shared/util/InfraredTool.js.map +1 -1
  146. package/dist/shared/util/Logger.js +85 -10
  147. package/dist/shared/util/Logger.js.map +1 -1
  148. package/dist/shared/util/TuyaStreamDelegate.js +194 -97
  149. package/dist/shared/util/TuyaStreamDelegate.js.map +1 -1
  150. package/dist/shared/util/util.js +59 -4
  151. package/dist/shared/util/util.js.map +1 -1
  152. package/docs/ADVANCED_OPTIONS.md +603 -0
  153. package/docs/CONFIG_GUIDE.md +616 -0
  154. package/docs/Get-Local-Keys-for-Your-Devices.md +246 -0
  155. package/{SUPPORTED_DEVICES.md → docs/SUPPORTED_DEVICES.md} +24 -3
  156. package/eslint.config.mjs +0 -1
  157. package/{jest.config.js → jest.config.ts} +5 -2
  158. package/package.json +12 -9
  159. package/test/ConfigMigrator.test.ts +1060 -0
  160. package/test/Logger.test.ts +482 -0
  161. package/test/accessoryFactory.data.test.ts +172 -0
  162. package/test/accessoryFactory.test.ts +64 -35
  163. package/test/airConditioner.test.ts +742 -0
  164. package/test/config.test.ts +17 -13
  165. package/test/custom.test.ts +2 -2
  166. package/test/errorHandling.test.ts +12 -12
  167. package/test/home.test.ts +1 -1
  168. package/test/hybridDeviceManager.test.ts +808 -0
  169. package/test/light.test.ts +250 -0
  170. package/test/localDevice.test.ts +21 -50
  171. package/test/localDeviceManager.behavior.test.ts +196 -31
  172. package/test/localDeviceManager.test.ts +124 -41
  173. package/test/mqttMessages.test.ts +33 -14
  174. package/test/platform.test.ts +337 -814
  175. package/test/platform.validation.local.test.ts +58 -119
  176. package/test/tuyaCustomDeviceManager.test.ts +106 -16
  177. package/test/tuyaDeviceManager.test.ts +636 -639
  178. package/test/tuyaDiscovery.test.ts +21 -33
  179. package/test/tuyaHomeDeviceManager.test.ts +109 -18
  180. package/test/tuyaOpenAPI.test.ts +25 -44
  181. package/test/tuyaOpenMQ.test.ts +15 -23
  182. package/test/util.test.ts +107 -7
  183. package/test/zigbee/LocalDevice.zigbee.test.ts +14 -18
  184. package/test/zigbee/LocalDeviceManager.cloud-discovery.test.ts +19 -20
  185. package/test/zigbee/LocalDeviceManager.zigbee.reconnect.test.ts +19 -20
  186. package/test/zigbee/LocalDeviceManager.zigbee.test.ts +73 -35
  187. package/test/zigbee/ZigbeeGatewayDetection.test.ts +2 -2
  188. package/ADVANCED_OPTIONS.md +0 -267
  189. package/dist/cloud/device/TuyaDeviceManager.js.map +0 -1
  190. package/dist/local/CloudLocalReconciliation.js +0 -180
  191. package/dist/local/CloudLocalReconciliation.js.map +0 -1
  192. package/dist/shared/AccessoryFactory.js +0 -276
  193. package/dist/shared/AccessoryFactory.js.map +0 -1
  194. package/dist/shared/accessories/AccessoryFactory.js +0 -305
  195. package/dist/shared/accessories/AccessoryFactory.js.map +0 -1
  196. package/dist/shared/accessories/AirConditionerAccessory.js.map +0 -1
  197. package/dist/shared/accessories/AirQualitySensorAccessory.js.map +0 -1
  198. package/dist/shared/accessories/BaseAccessory.js.map +0 -1
  199. package/dist/shared/accessories/CameraAccessory.js.map +0 -1
  200. package/dist/shared/accessories/GarageDoorAccessory.js.map +0 -1
  201. package/dist/shared/accessories/HumanPresenceSensorAccessory.js.map +0 -1
  202. package/dist/shared/accessories/IRAirConditionerAccessory.js.map +0 -1
  203. package/dist/shared/accessories/IRControlHubAccessory.js.map +0 -1
  204. package/dist/shared/accessories/IRControlHubSubAccessory.js.map +0 -1
  205. package/dist/shared/accessories/IRGenericAccessory.js.map +0 -1
  206. package/dist/shared/accessories/LocationWeatherAccessory.js.map +0 -1
  207. package/dist/shared/accessories/MotionSensorAccessory.js.map +0 -1
  208. package/dist/shared/accessories/OutletAccessory.js.map +0 -1
  209. package/dist/shared/accessories/SceneAccessory.js.map +0 -1
  210. package/dist/shared/accessories/SceneSwitchAccessory.js.map +0 -1
  211. package/dist/shared/accessories/SecuritySystemAccessory.js.map +0 -1
  212. package/dist/shared/accessories/SwitchAccessory.js.map +0 -1
  213. package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js.map +0 -1
  214. package/dist/shared/accessories/WeatherStationAccessory.js.map +0 -1
  215. package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js.map +0 -1
  216. package/dist/shared/accessories/WhiteNoiseLightAccessory.js.map +0 -1
  217. package/dist/shared/accessories/WindowAccessory.js.map +0 -1
  218. package/dist/shared/accessories/characteristic/Active.js.map +0 -1
  219. package/dist/shared/accessories/characteristic/AirQuality.js.map +0 -1
  220. package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js.map +0 -1
  221. package/dist/shared/accessories/characteristic/CurrentTemperature.js.map +0 -1
  222. package/dist/shared/accessories/characteristic/CurrentWeather.js.map +0 -1
  223. package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js.map +0 -1
  224. package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js.map +0 -1
  225. package/dist/shared/accessories/characteristic/EnergyUsage.js.map +0 -1
  226. package/dist/shared/accessories/characteristic/Light.js.map +0 -1
  227. package/dist/shared/accessories/characteristic/LightSensor.js.map +0 -1
  228. package/dist/shared/accessories/characteristic/LockPhysicalControls.js.map +0 -1
  229. package/dist/shared/accessories/characteristic/MotionDetected.js.map +0 -1
  230. package/dist/shared/accessories/characteristic/Name.js.map +0 -1
  231. package/dist/shared/accessories/characteristic/OccupancyDetected.js.map +0 -1
  232. package/dist/shared/accessories/characteristic/On.js.map +0 -1
  233. package/dist/shared/accessories/characteristic/OutletInUse.js.map +0 -1
  234. package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js.map +0 -1
  235. package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js.map +0 -1
  236. package/dist/shared/accessories/characteristic/RotationSpeed.js.map +0 -1
  237. package/dist/shared/accessories/characteristic/SecuritySystemState.js.map +0 -1
  238. package/dist/shared/accessories/characteristic/SwingMode.js.map +0 -1
  239. package/dist/shared/accessories/characteristic/TargetTemperature.js.map +0 -1
  240. package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js.map +0 -1
  241. package/test/zigbee/CloudLocalReconciliation.test.ts +0 -284
  242. package/tuya-local-configs/dev-1.hash +0 -1
  243. package/tuya-local-configs/device1.hash +0 -1
  244. package/wiki/Get-Local-Keys-for-Your-Devices.md +0 -85
  245. /package/dist/shared/{accessories → accessory}/AirPurifierAccessory.js +0 -0
  246. /package/dist/shared/{accessories → accessory}/AirQualitySensorAccessory.js +0 -0
  247. /package/dist/shared/{accessories → accessory}/BlindsAccessory.js +0 -0
  248. /package/dist/shared/{accessories → accessory}/CarbonDioxideSensorAccessory.js +0 -0
  249. /package/dist/shared/{accessories → accessory}/CarbonMonoxideSensorAccessory.js +0 -0
  250. /package/dist/shared/{accessories → accessory}/ContactSensorAccessory.js +0 -0
  251. /package/dist/shared/{accessories → accessory}/DehumidifierAccessory.js +0 -0
  252. /package/dist/shared/{accessories → accessory}/DiffuserAccessory.js +0 -0
  253. /package/dist/shared/{accessories → accessory}/DimmerAccessory.js +0 -0
  254. /package/dist/shared/{accessories → accessory}/DoorbellAccessory.js +0 -0
  255. /package/dist/shared/{accessories → accessory}/ExtractionHoodAccessory.js +0 -0
  256. /package/dist/shared/{accessories → accessory}/FanAccessory.js +0 -0
  257. /package/dist/shared/{accessories → accessory}/HeaterAccessory.js +0 -0
  258. /package/dist/shared/{accessories → accessory}/HeaterAccessory_old.js +0 -0
  259. /package/dist/shared/{accessories → accessory}/HumanPresenceSensorAccessory.js +0 -0
  260. /package/dist/shared/{accessories → accessory}/HumidifierAccessory.js +0 -0
  261. /package/dist/shared/{accessories → accessory}/IRAirConditionerAccessory.js +0 -0
  262. /package/dist/shared/{accessories → accessory}/IRControlHubAccessory.js +0 -0
  263. /package/dist/shared/{accessories → accessory}/IRControlHubSubAccessory.js +0 -0
  264. /package/dist/shared/{accessories → accessory}/IRGenericAccessory.js +0 -0
  265. /package/dist/shared/{accessories → accessory}/LeakSensorAccessory.js +0 -0
  266. /package/dist/shared/{accessories → accessory}/LightAccessory.js +0 -0
  267. /package/dist/shared/{accessories → accessory}/LightSensorAccessory.js +0 -0
  268. /package/dist/shared/{accessories → accessory}/MotionSensorAccessory.js +0 -0
  269. /package/dist/shared/{accessories → accessory}/OutletAccessory.js +0 -0
  270. /package/dist/shared/{accessories → accessory}/PetFeederAccessory.js +0 -0
  271. /package/dist/shared/{accessories → accessory}/SceneSwitchAccessory.js +0 -0
  272. /package/dist/shared/{accessories → accessory}/SecuritySystemAccessory.js +0 -0
  273. /package/dist/shared/{accessories → accessory}/SmokeSensorAccessory.js +0 -0
  274. /package/dist/shared/{accessories → accessory}/TemperatureHumiditySensorAccessory.js +0 -0
  275. /package/dist/shared/{accessories → accessory}/ThermostatAccessory.js +0 -0
  276. /package/dist/shared/{accessories → accessory}/TowerRackAccessory.js +0 -0
  277. /package/dist/shared/{accessories → accessory}/ValveAccessory.js +0 -0
  278. /package/dist/shared/{accessories → accessory}/VibrationSensorAccessory.js +0 -0
  279. /package/dist/shared/{accessories → accessory}/WeatherStationAccessory.js +0 -0
  280. /package/dist/shared/{accessories → accessory}/WetBulbGlobeTemperatureAccessory.js +0 -0
  281. /package/dist/shared/{accessories → accessory}/WindowAccessory.js +0 -0
  282. /package/dist/shared/{accessories → accessory}/WindowCoveringAccessory.js +0 -0
  283. /package/dist/shared/{accessories → accessory}/WirelessSwitchAccessory.js +0 -0
  284. /package/dist/shared/{accessories → accessory}/characteristic/Active.js +0 -0
  285. /package/dist/shared/{accessories → accessory}/characteristic/AirQuality.js +0 -0
  286. /package/dist/shared/{accessories → accessory}/characteristic/CurrentTemperature.js +0 -0
  287. /package/dist/shared/{accessories → accessory}/characteristic/CurrentWeather.js +0 -0
  288. /package/dist/shared/{accessories → accessory}/characteristic/CurrentWeatherByOpenMeteo.js +0 -0
  289. /package/dist/shared/{accessories → accessory}/characteristic/LightSensor.js +0 -0
  290. /package/dist/shared/{accessories → accessory}/characteristic/MotionDetected.js +0 -0
  291. /package/dist/shared/{accessories → accessory}/characteristic/Name.js +0 -0
  292. /package/dist/shared/{accessories → accessory}/characteristic/OccupancyDetected.js +0 -0
  293. /package/dist/shared/{accessories → accessory}/characteristic/OutletInUse.js +0 -0
  294. /package/dist/shared/{accessories → accessory}/characteristic/ProgrammableSwitchEvent.js +0 -0
  295. /package/dist/shared/{accessories → accessory}/characteristic/RotationSpeed.js +0 -0
  296. /package/dist/shared/{accessories → accessory}/characteristic/SecuritySystemState.js +0 -0
  297. /package/dist/shared/{accessories → accessory}/characteristic/TargetTemperature.js +0 -0
  298. /package/dist/shared/{accessories → accessory}/characteristic/TemperatureDisplayUnits.js +0 -0
@@ -1,6 +1,23 @@
1
1
  /* eslint-disable no-console */
2
2
  import { describe, expect, test, beforeEach, jest } from '@jest/globals';
3
3
  import { TuyaPlatform } from '../src/platform';
4
+ import { PlatformAccessory } from 'homebridge';
5
+ import { ExLogger, initLogger } from '../src/shared/util/Logger';
6
+ import { TuyaPluginMode } from '../src/config';
7
+ import LocalDeviceManager from '../src/local/LocalDeviceManager';
8
+ import TuyaHybridDeviceManager from '../src/shared/TuyaHybridDeviceManager';
9
+ import TuyaHomeDeviceManager from '../src/cloud/device/TuyaHomeDeviceManager';
10
+ import TuyaCustomDeviceManager from '../src/cloud/device/TuyaCustomDeviceManager';
11
+
12
+ // Mock Logger
13
+ const mockLogger: ExLogger = {
14
+ debug: jest.fn(),
15
+ info: jest.fn(),
16
+ warn: jest.fn(),
17
+ error: jest.fn(),
18
+ log: jest.fn(),
19
+ success: jest.fn(),
20
+ } as unknown as ExLogger;
4
21
 
5
22
  // Mock Homebridge API
6
23
  const mockHAP = {
@@ -11,6 +28,8 @@ const mockHAP = {
11
28
  },
12
29
  };
13
30
 
31
+ const errorStack:any[] = [];
32
+
14
33
  const mockAPI = {
15
34
  hap: mockHAP,
16
35
  platformAccessory: jest.fn(),
@@ -19,20 +38,15 @@ const mockAPI = {
19
38
  on: jest.fn(),
20
39
  } as any;
21
40
 
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
-
41
+ jest.mock('jsonschema', () => {
42
+ return {
43
+ Validator: (
44
+ class MockValidator {
45
+ validate = jest.fn().mockReturnValue({ errors: errorStack });
46
+ }
47
+ )
48
+ };
49
+ });
36
50
  // Mock file system
37
51
  jest.mock('fs', () => ({
38
52
  readFileSync: jest.fn(),
@@ -43,6 +57,10 @@ jest.mock('fs', () => ({
43
57
  jest.mock('../src/cloud/api/TuyaOpenAPI', () => {
44
58
  return class MockTuyaOpenAPI {
45
59
  log = console;
60
+
61
+ static getDefaultEndpoint() {
62
+ return 'mock-endpoint';
63
+ }
46
64
  };
47
65
  });
48
66
 
@@ -50,34 +68,65 @@ jest.mock('../src/cloud/api/TuyaOpenMQ', () => {
50
68
  return class MockTuyaOpenMQ {};
51
69
  });
52
70
 
53
- jest.mock('../src/cloud/device/TuyaDeviceManager', () => {
54
- return class MockTuyaDeviceManager {};
71
+ jest.mock('../src/cloud/device/TuyaCloudDeviceManager', () => {
72
+ return class MockTuyaDeviceManager {
73
+ pullDevices = jest.fn().mockReturnValue([]);
74
+ updateInfraredRemotes = jest.fn();
75
+ on = jest.fn();
76
+ devices = [];
77
+ };
55
78
  });
56
79
 
57
80
  jest.mock('../src/cloud/device/TuyaCustomDeviceManager', () => {
58
- return class MockTuyaCustomDeviceManager {};
81
+ return class MockTuyaCustomDeviceManager {
82
+ pullDevices = jest.fn().mockReturnValue([]);
83
+ updateInfraredRemotes = jest.fn();
84
+ on = jest.fn();
85
+ devices = [];
86
+ };
59
87
  });
60
88
 
61
89
  jest.mock('../src/cloud/device/TuyaHomeDeviceManager', () => {
62
- return class MockTuyaHomeDeviceManager {};
90
+ return class MockTuyaHomeDeviceManager {
91
+ pullDevices = jest.fn().mockReturnValue([]);
92
+ updateInfraredRemotes = jest.fn();
93
+ on = jest.fn();
94
+ devices = [];
95
+ };
63
96
  });
64
97
 
65
98
  // Mock Local components
66
99
  jest.mock('../src/local/LocalDeviceManager', () => {
67
- return class MockLocalDeviceManager {};
100
+ return class MockLocalDeviceManager {
101
+ pullDevices = jest.fn().mockReturnValue([]);
102
+ updateInfraredRemotes = jest.fn();
103
+ on = jest.fn();
104
+ devices = [];
105
+ static isTargetDevice:any = jest.fn(s => true);
106
+ };
107
+ });
108
+
109
+ jest.mock('../src/shared/TuyaHybridDeviceManager', () => {
110
+ return class MockTuyaHybridDeviceManager {
111
+ pullDevices = jest.fn().mockReturnValue([]);
112
+ updateInfraredRemotes = jest.fn();
113
+ on = jest.fn();
114
+ devices = [];
115
+ };
68
116
  });
69
117
 
70
118
  // Mock Accessories
71
- jest.mock('../src/shared/accessories/AccessoryFactory', () => {
119
+ jest.mock('../src/shared/accessory/AccessoryFactory', () => {
72
120
  return {
73
121
  default: class MockAccessoryFactory {},
74
122
  };
75
123
  });
76
124
 
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
- }));
125
+ jest.mock('../src/shared/util/ConfigMigrator', () => {
126
+ return class ConfigMigrator {
127
+ migrate(arg:any) { return arg; }
128
+ }
129
+ });
81
130
 
82
131
  describe('TuyaPlatform', () => {
83
132
  let platform: TuyaPlatform;
@@ -85,18 +134,22 @@ describe('TuyaPlatform', () => {
85
134
  let mockLog: any;
86
135
 
87
136
  beforeEach(() => {
137
+ jest.clearAllMocks();
138
+ initLogger(mockLogger);
139
+
88
140
  mockLog = {
89
141
  debug: jest.fn(),
90
142
  info: jest.fn(),
91
143
  warn: jest.fn(),
92
144
  error: jest.fn(),
145
+ success: jest.fn(),
93
146
  };
94
147
 
95
148
  mockConfig = {
96
149
  platform: 'TuyaPlatform',
97
150
  name: 'Tuya',
98
151
  mode: 'cloud',
99
- options: {
152
+ cloud: {
100
153
  projectType: '2',
101
154
  accessId: 'test_id',
102
155
  accessKey: 'test_key',
@@ -109,6 +162,8 @@ describe('TuyaPlatform', () => {
109
162
  },
110
163
  };
111
164
 
165
+ process.removeAllListeners('uncaughtException');
166
+ process.removeAllListeners('unhandledRejection');
112
167
  platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
113
168
  });
114
169
 
@@ -136,8 +191,8 @@ describe('TuyaPlatform', () => {
136
191
  });
137
192
 
138
193
  test('initializes cached accessories array', () => {
139
- expect(platform.cachedAccessories).toEqual([]);
140
- expect(Array.isArray(platform.cachedAccessories)).toBe(true);
194
+ expect(platform['platformAccessories']).toEqual(new Map());
195
+ expect(platform['platformAccessories'] instanceof Map).toBe(true);
141
196
  });
142
197
 
143
198
  test('initializes accessory handlers array', () => {
@@ -155,7 +210,7 @@ describe('TuyaPlatform', () => {
155
210
  const localConfig = {
156
211
  ...mockConfig,
157
212
  mode: 'local',
158
- options: undefined,
213
+ cloud: undefined,
159
214
  local: {
160
215
  devices: [],
161
216
  },
@@ -180,16 +235,16 @@ describe('TuyaPlatform', () => {
180
235
  });
181
236
 
182
237
  describe('configuration validation', () => {
183
- test('requires options for cloud mode', () => {
184
- const noOptionsConfig = {
238
+ test('requires cloud for cloud mode', () => {
239
+ const noCloudConfig = {
185
240
  platform: 'TuyaPlatform',
186
241
  name: 'Tuya',
187
242
  mode: 'cloud',
188
- options: undefined,
243
+ cloud: undefined,
189
244
  };
190
245
 
191
- const noOptionsPlatform = new TuyaPlatform(mockLog, noOptionsConfig, mockAPI);
192
- const isValid = noOptionsPlatform.validate();
246
+ const noCloudPlatform = new TuyaPlatform(mockLog, noCloudConfig, mockAPI);
247
+ const isValid = noCloudPlatform.validate();
193
248
 
194
249
  expect(isValid).toBe(false);
195
250
  });
@@ -213,7 +268,7 @@ describe('TuyaPlatform', () => {
213
268
  platform: 'TuyaPlatform',
214
269
  name: 'Tuya',
215
270
  mode: 'cloud',
216
- options: {
271
+ cloud: {
217
272
  projectType: '1',
218
273
  endpoint: 'https://openapi.tuyaeu.com',
219
274
  accessId: 'id',
@@ -226,7 +281,7 @@ describe('TuyaPlatform', () => {
226
281
  };
227
282
 
228
283
  const cloudPlatform = new TuyaPlatform(mockLog, cloudConfig, mockAPI);
229
- expect(cloudPlatform.options.projectType).toBe('1');
284
+ expect(cloudPlatform.platformConfig!.cloud!.projectType).toBe('1');
230
285
  });
231
286
 
232
287
  test('validates cloud config with projectType 2', () => {
@@ -234,7 +289,7 @@ describe('TuyaPlatform', () => {
234
289
  platform: 'TuyaPlatform',
235
290
  name: 'Tuya',
236
291
  mode: 'cloud',
237
- options: {
292
+ cloud: {
238
293
  projectType: '2',
239
294
  accessId: 'id',
240
295
  accessKey: 'key',
@@ -248,7 +303,7 @@ describe('TuyaPlatform', () => {
248
303
  };
249
304
 
250
305
  const cloudPlatform = new TuyaPlatform(mockLog, cloudConfig, mockAPI);
251
- expect(cloudPlatform.options.projectType).toBe('2');
306
+ expect(cloudPlatform.platformConfig!.cloud!.projectType).toBe('2');
252
307
  });
253
308
  });
254
309
 
@@ -295,13 +350,13 @@ describe('TuyaPlatform', () => {
295
350
  });
296
351
 
297
352
  test('stores cloud options', () => {
298
- expect(platform.options).toBeDefined();
299
- expect(platform.options.projectType).toBe('2');
353
+ expect(platform.platformConfig.cloud).toBeDefined();
354
+ expect(platform.platformConfig!.cloud!.projectType).toBe('2');
300
355
  });
301
356
 
302
357
  test('accesses credentials from config', () => {
303
- expect(platform.options.accessId).toBe('test_id');
304
- expect(platform.options.accessKey).toBe('test_key');
358
+ expect(platform.platformConfig!.cloud!.accessId).toBe('test_id');
359
+ expect(platform.platformConfig!.cloud!.accessKey).toBe('test_key');
305
360
  });
306
361
  });
307
362
 
@@ -329,38 +384,62 @@ describe('TuyaPlatform', () => {
329
384
 
330
385
  test('caches accessories', () => {
331
386
  const mockAccessory = { UUID: 'uuid-1', displayName: 'Cached Device' };
332
- platform.cachedAccessories.push(mockAccessory as any);
387
+ platform.configureAccessory(mockAccessory as PlatformAccessory);
333
388
 
334
- expect(platform.cachedAccessories.length).toBe(1);
389
+ expect(platform['platformAccessories'].size).toBe(1);
335
390
  });
336
391
 
337
392
  test('can restore cached accessories', () => {
338
393
  const mockAccessory1 = { UUID: 'uuid-1', displayName: 'Device 1' };
339
394
  const mockAccessory2 = { UUID: 'uuid-2', displayName: 'Device 2' };
395
+ [mockAccessory1, mockAccessory2].forEach(mockAccessory => platform.configureAccessory(mockAccessory as PlatformAccessory));
340
396
 
341
- platform.cachedAccessories = [mockAccessory1 as any, mockAccessory2 as any];
342
-
343
- expect(platform.cachedAccessories.length).toBe(2);
397
+ expect(platform['platformAccessories'].size).toBe(2);
344
398
  });
345
399
  });
346
400
 
347
401
  describe('device manager initialization', () => {
348
- test('can initialize cloud device manager', () => {
402
+ test('can initialize device manager', () => {
349
403
  platform.deviceManager = {} as any;
350
404
  expect(platform.deviceManager).toBeDefined();
351
405
  });
352
406
 
353
- test('can initialize local device manager', () => {
354
- platform.localDeviceManager = {} as any;
355
- expect(platform.localDeviceManager).toBeDefined();
407
+ test('initialize custom device manager', () => {
408
+ mockConfig.mode = TuyaPluginMode.cloud;
409
+ mockConfig.cloud.projectType = '1';
410
+ const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
411
+ platform.initDevices().then(() => {
412
+ expect(platform.mode).toBe(mockConfig.mode);
413
+ expect(platform.deviceManager).toBeInstanceOf(TuyaCustomDeviceManager);
414
+ });
356
415
  });
357
416
 
358
- test('both managers can coexist in both mode', () => {
359
- platform.deviceManager = {} as any;
360
- platform.localDeviceManager = {} as any;
417
+ test('initialize home device manager', () => {
418
+ mockConfig.mode = TuyaPluginMode.cloud;
419
+ mockConfig.cloud.projectType = '2';
420
+ const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
421
+ platform.initDevices().then(() => {
422
+ expect(platform.mode).toBe(mockConfig.mode);
423
+ expect(platform.deviceManager).toBeInstanceOf(TuyaHomeDeviceManager);
424
+ });
425
+ });
361
426
 
362
- expect(platform.deviceManager).toBeDefined();
363
- expect(platform.localDeviceManager).toBeDefined();
427
+ test('initialize local device manager', () => {
428
+ mockConfig.mode = TuyaPluginMode.local;
429
+ const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
430
+ platform.initDevices().then(() => {
431
+ expect(platform.mode).toBe(mockConfig.mode);
432
+ expect(platform.deviceManager).toBeInstanceOf(LocalDeviceManager);
433
+ });
434
+ });
435
+
436
+ test('initialize both device manager', () => {
437
+ mockConfig.mode = TuyaPluginMode.both;
438
+ const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
439
+ platform.initDevices().then(() => {
440
+ expect(platform.mode).toBe(mockConfig.mode);
441
+ expect(platform.deviceManager).toBeInstanceOf(TuyaHybridDeviceManager);
442
+ });
364
443
  });
365
444
  });
366
445
 
@@ -370,7 +449,7 @@ describe('TuyaPlatform', () => {
370
449
  platform: 'TuyaPlatform',
371
450
  name: 'Tuya',
372
451
  mode: 'both',
373
- options: {
452
+ cloud: {
374
453
  projectType: '2',
375
454
  accessId: 'id',
376
455
  accessKey: 'key',
@@ -388,183 +467,8 @@ describe('TuyaPlatform', () => {
388
467
 
389
468
  const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
390
469
  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)
470
+ expect(bothPlatform.platformConfig.cloud).toBeDefined();
564
471
  expect(bothPlatform.platformConfig.local).toBeDefined();
565
- if (bothPlatform.platformConfig.local?.devices) {
566
- expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
567
- }
568
472
  });
569
473
  });
570
474
 
@@ -624,13 +528,135 @@ describe('TuyaPlatform', () => {
624
528
  });
625
529
  });
626
530
 
531
+ describe('validate', () => {
532
+ test('local mode without local config', () => {
533
+ const invalidConfig = {
534
+ platform: 'TuyaPlatform',
535
+ name: 'Tuya',
536
+ mode: 'local',
537
+ };
538
+
539
+ const spyError = jest.spyOn(mockLog, 'error');
540
+
541
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
542
+ const isValid = invalidPlatform.validate();
543
+
544
+ expect(isValid).toBe(false);
545
+ expect(spyError).toHaveBeenCalledWith('mode is "local" but no "local" config block found.');
546
+ });
547
+
548
+ test('both mode without local config', () => {
549
+ const invalidConfig = {
550
+ platform: 'TuyaPlatform',
551
+ name: 'Tuya',
552
+ mode: 'both',
553
+ };
554
+
555
+ const spyError = jest.spyOn(mockLog, 'error');
556
+
557
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
558
+ const isValid = invalidPlatform.validate();
559
+
560
+ expect(isValid).toBe(false);
561
+ expect(spyError).toHaveBeenCalledWith('mode is "both" but no "local" config block found.');
562
+ });
563
+
564
+ test('cloud mode without cloud config', () => {
565
+ const invalidConfig = {
566
+ platform: 'TuyaPlatform',
567
+ name: 'Tuya',
568
+ mode: 'cloud',
569
+ };
570
+
571
+ const spyError = jest.spyOn(mockLog, 'error');
572
+
573
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
574
+ const isValid = invalidPlatform.validate();
575
+
576
+ expect(isValid).toBe(false);
577
+ expect(spyError).toHaveBeenCalledWith('Not configured — "cloud" block is required for cloud mode, exit.');
578
+ });
579
+
580
+ test('both mode without cloud config', () => {
581
+ const invalidConfig = {
582
+ platform: 'TuyaPlatform',
583
+ name: 'Tuya',
584
+ mode: 'both',
585
+ local: {}
586
+ };
587
+
588
+ const spyError = jest.spyOn(mockLog, 'error');
589
+
590
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
591
+ const isValid = invalidPlatform.validate();
592
+
593
+ expect(isValid).toBe(false);
594
+ expect(spyError).toHaveBeenCalledWith('Not configured — "cloud" block is required for cloud mode, exit.');
595
+ });
596
+
597
+ test('Validator().validate returns error', () => {
598
+ const invalidConfig = {
599
+ platform: 'TuyaPlatform',
600
+ name: 'Tuya',
601
+ mode: 'cloud',
602
+ cloud: { projectType: '1', deviceOverides: [] }
603
+ };
604
+
605
+ const stack = { stack: 'error stack' };
606
+ errorStack.push(stack);
607
+
608
+ const spyError = jest.spyOn(mockLog, 'error');
609
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
610
+
611
+ expect(spyError).toHaveBeenCalledWith(stack.stack);
612
+ errorStack.splice(0, errorStack.length);
613
+ });
614
+
615
+ test('cloud mode with invalid deviceOverrides', () => {
616
+ const invalidConfig = {
617
+ platform: 'TuyaPlatform',
618
+ name: 'Tuya',
619
+ mode: 'cloud',
620
+ cloud: { projectType: '1' }
621
+ };
622
+
623
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
624
+ invalidPlatform.validateDeviceOverrides = jest.fn(() => false);
625
+ const spyMethod = jest.spyOn(invalidPlatform, 'validateDeviceOverrides');
626
+ const isValid = invalidPlatform.validate();
627
+
628
+ expect(spyMethod).toHaveBeenCalledTimes(1);
629
+ expect(isValid).toBe(false);
630
+ });
631
+
632
+ test('cloud mode with invalid deviceOverride.schema', () => {
633
+ const invalidConfig = {
634
+ platform: 'TuyaPlatform',
635
+ name: 'Tuya',
636
+ mode: 'cloud',
637
+ cloud: { projectType: '1' }
638
+ };
639
+
640
+ const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
641
+ invalidPlatform.validateDeviceOverrides = jest.fn(() => true);
642
+ invalidPlatform.validateSchema = jest.fn(() => false);
643
+ const spyMethod1 = jest.spyOn(invalidPlatform, 'validateDeviceOverrides');
644
+ const spyMethod2 = jest.spyOn(invalidPlatform, 'validateSchema');
645
+ const isValid = invalidPlatform.validate();
646
+
647
+ expect(spyMethod1).toHaveBeenCalledTimes(1);
648
+ expect(spyMethod2).toHaveBeenCalledTimes(1);
649
+ expect(isValid).toBe(false);
650
+ });
651
+ });
652
+
627
653
  describe('configuration edge cases', () => {
628
654
  test('handles missing project type', () => {
629
655
  const noProjectTypeConfig = {
630
656
  platform: 'TuyaPlatform',
631
657
  name: 'Tuya',
632
658
  mode: 'cloud',
633
- options: {
659
+ cloud: {
634
660
  accessId: 'id',
635
661
  accessKey: 'key',
636
662
  },
@@ -657,15 +683,15 @@ describe('TuyaPlatform', () => {
657
683
  test('handles weather accessory option', () => {
658
684
  const weatherConfig = {
659
685
  ...mockConfig,
660
- options: {
661
- ...mockConfig.options,
686
+ cloud: {
687
+ ...mockConfig.cloud,
662
688
  generateWeatherAccessory: true,
663
689
  weatherAPI: 'openweathermap',
664
690
  },
665
691
  };
666
692
 
667
693
  const weatherPlatform = new TuyaPlatform(mockLog, weatherConfig, mockAPI);
668
- expect((weatherPlatform.options as any).generateWeatherAccessory).toBe(true);
694
+ expect((weatherPlatform.platformConfig.cloud as any).generateWeatherAccessory).toBe(true);
669
695
  });
670
696
  });
671
697
 
@@ -684,20 +710,21 @@ describe('TuyaPlatform', () => {
684
710
  const platform1 = new TuyaPlatform(mockLog, config1, mockAPI);
685
711
  const platform2 = new TuyaPlatform(mockLog, config2, mockAPI);
686
712
 
687
- platform1.cachedAccessories.push({} as any);
713
+ platform1.configureAccessory({} as PlatformAccessory);
714
+
688
715
 
689
- expect(platform1.cachedAccessories.length).toBe(1);
690
- expect(platform2.cachedAccessories.length).toBe(0);
716
+ expect(platform1['platformAccessories'].size).toBe(1);
717
+ expect(platform2['platformAccessories'].size).toBe(0);
691
718
  });
692
719
  });
693
720
 
694
- describe('API field variations and error handling', () => {
695
- test('handles localKey instead of local_key in API response', async () => {
721
+ describe('device ID consistency and deduplication', () => {
722
+ test('uses consistent UUID for same device across cloud and local', () => {
696
723
  const bothConfig = {
697
724
  platform: 'TuyaPlatform',
698
725
  name: 'Tuya',
699
726
  mode: 'both',
700
- options: {
727
+ cloud: {
701
728
  projectType: '2',
702
729
  accessId: 'id',
703
730
  accessKey: 'key',
@@ -715,595 +742,91 @@ describe('TuyaPlatform', () => {
715
742
 
716
743
  const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
717
744
 
745
+ // Create mock cloud and local devices with same ID
718
746
  const mockCloudDevice = {
719
- id: 'test-device-123',
720
- uuid: 'test-device-123',
721
- name: 'Test Device',
722
- product_id: 'product-123',
747
+ id: 'same-device-id',
748
+ uuid: 'same-device-id',
749
+ name: 'Test Device (Cloud)',
723
750
  } as any;
724
751
 
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
- }),
752
+ const mockLocalDevice = {
753
+ id: 'same-device-id',
754
+ uuid: 'same-device-id',
755
+ name: 'Test Device (Local)',
735
756
  } as any;
736
757
 
737
- await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
758
+ // Both should map to same UUID
759
+ const cloudUUID = bothPlatform.api.hap.uuid.generate(mockCloudDevice.id);
760
+ const localUUID = bothPlatform.api.hap.uuid.generate(mockLocalDevice.id);
738
761
 
739
- if (bothPlatform.platformConfig.local?.devices) {
740
- const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
741
- expect(enrichedDevice.tuyaKey).toBe('alternate-format-key');
742
- }
762
+ expect(cloudUUID).toBe(localUUID);
743
763
  });
764
+ });
744
765
 
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
- },
766
+ describe('syncDeviceAndAccessory', () => {
767
+ test('device config changed', () => {
768
+ const mockDevice = {
769
+ id: 'mock-id'
764
770
  };
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',
771
+ const mockOldAccessory = {
772
+ context: { deviceConfigHash: 'hash2' },
773
+ services: [
774
+ {
775
+ UUID: 'service-uuid1',
776
+ characteristics: [
777
+ { UUID: 'characteristic-uuid1' },
778
+ { UUID: 'characteristic-uuid2' },
779
+ { UUID: 'characteristic-uuid3' },
780
+ ],
781
+ removeCharacteristic: jest.fn()
827
782
  },
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
- },
783
+ {
784
+ UUID: 'service-uuid2',
785
+ characteristics: [
786
+ { UUID: 'characteristic-uuid1' },
787
+ { UUID: 'characteristic-uuid3' },
788
+ { UUID: 'characteristic-uuid4' },
789
+ ]
790
+ }
791
+ ],
792
+ removeService: jest.fn()
1063
793
  };
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',
794
+ const mockNewAccessory = {
795
+ services: [
796
+ {
797
+ UUID: 'service-uuid1',
798
+ characteristics: [
799
+ { UUID: 'characteristic-uuid1' },
800
+ { UUID: 'characteristic-uuid3' },
801
+ { UUID: 'characteristic-uuid5' },
802
+ ]
1079
803
  },
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' };
804
+ {
805
+ UUID: 'service-uuid3',
806
+ characteristics: [
807
+ { UUID: 'characteristic-uuid2' },
808
+ { UUID: 'characteristic-uuid3' },
809
+ { UUID: 'characteristic-uuid4' },
810
+ ]
1186
811
  }
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
- },
812
+ ]
1278
813
  };
1279
814
 
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;
815
+ Object.assign(mockAPI, { updatePlatformAccessories: jest.fn() });
1299
816
 
1300
- await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
817
+ const mockPlatform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
818
+ Object.assign(mockPlatform, {
819
+ deviceManager: { createDeviceConfigHash: jest.fn().mockReturnValue('hash1'), devices: []},
820
+ addAccessory: jest.fn().mockReturnValue(mockNewAccessory),
821
+ removeAccessory: jest.fn()
822
+ });
823
+
824
+ mockPlatform.syncDeviceAndAccessory(mockDevice as any, mockOldAccessory as any);
1301
825
 
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
- }
826
+ const mockRemoveService = jest.spyOn(mockOldAccessory, 'removeService');
827
+ const mockRemoveCharacteristic = jest.spyOn(mockOldAccessory.services[0], 'removeCharacteristic');
828
+ expect(mockRemoveService).toHaveBeenCalledWith(mockOldAccessory.services[1]);
829
+ expect(mockRemoveCharacteristic).toHaveBeenCalledWith(mockOldAccessory.services[0].characteristics[1]);
1307
830
  });
1308
831
  });
1309
832
  });