@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
@@ -0,0 +1,808 @@
1
+ /* eslint-disable no-console */
2
+ import { describe, expect, test, beforeEach, afterEach, jest } from '@jest/globals';
3
+ import { LocalConfig } from '../src/local/config';
4
+ import { TuyaPlatformCloudConfig } from '../src/config';
5
+ import TuyaHybridDeviceManager from '../src/shared/TuyaHybridDeviceManager';
6
+ import { ExLogger, initLogger } from '../src/shared/util/Logger';
7
+
8
+ describe('HybridDeviceManager', () => {
9
+ let manager: TuyaHybridDeviceManager;
10
+ let mockAPI: any;
11
+
12
+ beforeEach(() => {
13
+ // Mock Logger
14
+ const mockLog: ExLogger = {
15
+ debug: jest.fn(),
16
+ info: jest.fn(),
17
+ warn: jest.fn(),
18
+ error: jest.fn(),
19
+ log: jest.fn(),
20
+ success: jest.fn(),
21
+ } as unknown as ExLogger;
22
+
23
+ initLogger(mockLog);
24
+
25
+ mockAPI = {
26
+ log: { log: mockLog } as any,
27
+ post: jest.fn(),
28
+ get: jest.fn(),
29
+ tokenInfo: { uid: 'test_uid' },
30
+ };
31
+
32
+ const mockCloudConfig: TuyaPlatformCloudConfig = {
33
+ projectType: '2',
34
+ accessId: 'test_id',
35
+ accessKey: 'test_key',
36
+ countryCode: 1,
37
+ username: 'user@example.com',
38
+ password: 'password',
39
+ appSchema: 'tuyaSmart',
40
+ generateWeatherAccessory: false,
41
+ weatherAPI: '',
42
+ forceIPv4: false,
43
+ };
44
+ const config: LocalConfig = {
45
+ devices: [],
46
+ };
47
+ manager = new TuyaHybridDeviceManager(mockAPI, mockCloudConfig, config);
48
+ });
49
+
50
+ afterEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ describe('initialization', () => {
55
+ test('creates manager instance with config', () => {
56
+ expect(manager).toBeDefined();
57
+ expect(manager).toBeInstanceOf(TuyaHybridDeviceManager);
58
+ });
59
+
60
+ test('initializes with empty devices', () => {
61
+ expect(manager.devices.length).toBe(0);
62
+ });
63
+
64
+ test('stops MQTT on initialization', () => {
65
+ // MQTT should be stopped during construction
66
+ expect(manager).toBeDefined();
67
+ });
68
+ });
69
+
70
+ describe('enrichLocalConfigFromCloud', () => {
71
+ test('enriches local config with cloud device details', async () => {
72
+ const bothConfig = {
73
+ platform: 'TuyaPlatform',
74
+ name: 'Tuya',
75
+ mode: 'both',
76
+ cloud: {
77
+ projectType: '2',
78
+ accessId: 'id',
79
+ accessKey: 'key',
80
+ countryCode: 1,
81
+ username: 'user',
82
+ password: 'pass',
83
+ appSchema: 'tuyaSmart',
84
+ generateWeatherAccessory: false,
85
+ weatherAPI: '',
86
+ } as TuyaPlatformCloudConfig,
87
+ local: {
88
+ devices: [],
89
+ },
90
+ };
91
+
92
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
93
+
94
+ // Mock cloud device
95
+ const mockCloudDevice = {
96
+ id: 'test-device-123',
97
+ uuid: 'test-device-123',
98
+ name: 'Test Device',
99
+ product_id: 'product-123',
100
+ local_key: 'test-local-key-abcdef',
101
+ isVirtualDevice: jest.fn(),
102
+ } as any;
103
+
104
+ // Enrich local config
105
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
106
+
107
+ // Verify local config was enriched
108
+ expect(enrichedLocalConfig).toBeDefined();
109
+ if (enrichedLocalConfig?.devices) {
110
+ expect(enrichedLocalConfig.devices.length).toBeGreaterThan(0);
111
+
112
+ const enrichedDevice = enrichedLocalConfig.devices[0];
113
+ expect(enrichedDevice.tuyaDeviceId).toBe('test-device-123');
114
+ expect(enrichedDevice.tuyaKey).toBe('test-local-key-abcdef');
115
+ }
116
+ });
117
+
118
+ test('preserves manual local config when enriching', async () => {
119
+ const bothConfig = {
120
+ platform: 'TuyaPlatform',
121
+ name: 'Tuya',
122
+ mode: 'both',
123
+ cloud: {
124
+ projectType: '2',
125
+ accessId: 'id',
126
+ accessKey: 'key',
127
+ countryCode: 1,
128
+ username: 'user',
129
+ password: 'pass',
130
+ appSchema: 'tuyaSmart',
131
+ generateWeatherAccessory: false,
132
+ weatherAPI: '',
133
+ } as TuyaPlatformCloudConfig,
134
+ local: {
135
+ devices: [
136
+ {
137
+ tuyaDeviceId: 'manual-device-456',
138
+ tuyaKey: 'manual-key-xyz',
139
+ name: 'Manual Device',
140
+ },
141
+ ],
142
+ },
143
+ };
144
+
145
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
146
+
147
+ // Mock cloud device (different from manual one)
148
+ const mockCloudDevice = {
149
+ id: 'test-device-123',
150
+ uuid: 'test-device-123',
151
+ name: 'Test Device',
152
+ product_id: 'product-123',
153
+ ip: '192.168.1.100',
154
+ local_key: 'test-local-key-abcdef',
155
+ isVirtualDevice: jest.fn(),
156
+ } as any;
157
+
158
+ // Enrich local config
159
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
160
+
161
+ // Verify local config exists and has devices
162
+ expect(enrichedLocalConfig).toBeDefined();
163
+ if (enrichedLocalConfig?.devices) {
164
+ // Verify manual device is still there
165
+ const manualDevice = enrichedLocalConfig.devices.find(
166
+ d => d.tuyaDeviceId === 'manual-device-456'
167
+ );
168
+ expect(manualDevice).toBeDefined();
169
+ expect(manualDevice?.tuyaKey).toBe('manual-key-xyz');
170
+
171
+ // Verify new cloud device was added
172
+ const cloudDevice = enrichedLocalConfig.devices.find(
173
+ d => d.tuyaDeviceId === 'test-device-123'
174
+ );
175
+ expect(cloudDevice).toBeDefined();
176
+ expect(cloudDevice?.tuyaKey).toBe('test-local-key-abcdef');
177
+ }
178
+ });
179
+
180
+ test('skips enrichment if cloud device details unavailable', async () => {
181
+ const bothConfig = {
182
+ platform: 'TuyaPlatform',
183
+ name: 'Tuya',
184
+ mode: 'both',
185
+ cloud: {
186
+ projectType: '2',
187
+ accessId: 'id',
188
+ accessKey: 'key',
189
+ countryCode: 1,
190
+ username: 'user',
191
+ password: 'pass',
192
+ appSchema: 'tuyaSmart',
193
+ generateWeatherAccessory: false,
194
+ weatherAPI: '',
195
+ } as TuyaPlatformCloudConfig,
196
+ local: {
197
+ devices: [],
198
+ },
199
+ };
200
+
201
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
202
+
203
+ const mockCloudDevice = {
204
+ id: 'test-device-123',
205
+ name: 'Test Device',
206
+ isVirtualDevice: jest.fn(),
207
+ } as any;
208
+
209
+ // Enrich local config
210
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
211
+
212
+ // Verify nothing was added (since API failed)
213
+ expect(enrichedLocalConfig).toBeDefined();
214
+ if (enrichedLocalConfig?.devices) {
215
+ expect(enrichedLocalConfig.devices.length).toBe(0);
216
+ }
217
+ });
218
+ });
219
+
220
+ describe('API field variations and error handling', () => {
221
+ test('handles localKey instead of local_key in API response', async () => {
222
+ const bothConfig = {
223
+ platform: 'TuyaPlatform',
224
+ name: 'Tuya',
225
+ mode: 'both',
226
+ cloud: {
227
+ projectType: '2',
228
+ accessId: 'id',
229
+ accessKey: 'key',
230
+ countryCode: 1,
231
+ username: 'user',
232
+ password: 'pass',
233
+ appSchema: 'tuyaSmart',
234
+ generateWeatherAccessory: false,
235
+ weatherAPI: '',
236
+ } as TuyaPlatformCloudConfig,
237
+ local: {
238
+ devices: [],
239
+ },
240
+ };
241
+
242
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
243
+
244
+ const mockCloudDevice = {
245
+ id: 'test-device-123',
246
+ uuid: 'test-device-123',
247
+ name: 'Test Device',
248
+ product_id: 'product-123',
249
+ local_key: 'test-local-key-abcdef',
250
+ isVirtualDevice: jest.fn(),
251
+ } as any;
252
+
253
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
254
+
255
+ if (enrichedLocalConfig?.devices) {
256
+ const enrichedDevice = enrichedLocalConfig.devices[0];
257
+ expect(enrichedDevice.tuyaKey).toBe('test-local-key-abcdef');
258
+ }
259
+ });
260
+
261
+ test('uses uuid fallback when cloud device id is missing', async () => {
262
+ const bothConfig = {
263
+ platform: 'TuyaPlatform',
264
+ name: 'Tuya',
265
+ mode: 'both',
266
+ cloud: {
267
+ projectType: '2',
268
+ accessId: 'id',
269
+ accessKey: 'key',
270
+ countryCode: 1,
271
+ username: 'user',
272
+ password: 'pass',
273
+ appSchema: 'tuyaSmart',
274
+ generateWeatherAccessory: false,
275
+ weatherAPI: '',
276
+ } as TuyaPlatformCloudConfig,
277
+ local: {
278
+ devices: [],
279
+ },
280
+ };
281
+
282
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
283
+
284
+ const mockCloudDevice = {
285
+ uuid: 'fallback-device-123',
286
+ name: 'Fallback Device',
287
+ local_key: 'fallback-key-123',
288
+ isVirtualDevice: jest.fn(),
289
+ } as any;
290
+
291
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
292
+
293
+ if (enrichedLocalConfig?.devices) {
294
+ const enrichedDevice = enrichedLocalConfig.devices[0];
295
+ expect(enrichedDevice.tuyaDeviceId).toBe('fallback-device-123');
296
+ expect(enrichedDevice.tuyaKey).toBe('fallback-key-123');
297
+ }
298
+ });
299
+
300
+ test('propagates cloud category into local config', async () => {
301
+ const bothConfig = {
302
+ platform: 'TuyaPlatform',
303
+ name: 'Tuya',
304
+ mode: 'both',
305
+ cloud: {
306
+ projectType: '2',
307
+ accessId: 'id',
308
+ accessKey: 'key',
309
+ countryCode: 1,
310
+ username: 'user',
311
+ password: 'pass',
312
+ appSchema: 'tuyaSmart',
313
+ generateWeatherAccessory: false,
314
+ weatherAPI: '',
315
+ } as TuyaPlatformCloudConfig,
316
+ local: {
317
+ devices: [],
318
+ },
319
+ };
320
+
321
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
322
+
323
+
324
+ const mockCloudDevice = {
325
+ id: 'category-device-123',
326
+ name: 'Category Device',
327
+ category: 'kg',
328
+ product_id: 'abc123',
329
+ local_key: 'test-local-key-abcdef',
330
+ isVirtualDevice: jest.fn(),
331
+ } as any;
332
+
333
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
334
+
335
+ if (enrichedLocalConfig?.devices) {
336
+ const enrichedDevice = enrichedLocalConfig.devices[0];
337
+ expect(enrichedDevice.category).toBe('kg');
338
+ }
339
+ });
340
+
341
+ test('handles missing local_key gracefully', async () => {
342
+ const bothConfig = {
343
+ platform: 'TuyaPlatform',
344
+ name: 'Tuya',
345
+ mode: 'both',
346
+ cloud: {
347
+ projectType: '2',
348
+ accessId: 'id',
349
+ accessKey: 'key',
350
+ countryCode: 1,
351
+ username: 'user',
352
+ password: 'pass',
353
+ appSchema: 'tuyaSmart',
354
+ generateWeatherAccessory: false,
355
+ weatherAPI: '',
356
+ } as TuyaPlatformCloudConfig,
357
+ local: {
358
+ devices: [],
359
+ },
360
+ };
361
+
362
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
363
+
364
+ const mockCloudDevice = {
365
+ id: 'test-device-no-key',
366
+ name: 'Device Without Local Key',
367
+ isVirtualDevice: jest.fn(),
368
+ } as any;
369
+
370
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
371
+
372
+ // Device should not be added to local config without local_key
373
+ if (enrichedLocalConfig?.devices) {
374
+ expect(enrichedLocalConfig.devices.length).toBe(0);
375
+ }
376
+ });
377
+
378
+ test('handles API timeout with grace', async () => {
379
+ const bothConfig = {
380
+ platform: 'TuyaPlatform',
381
+ name: 'Tuya',
382
+ mode: 'both',
383
+ cloud: {
384
+ projectType: '2',
385
+ accessId: 'id',
386
+ accessKey: 'key',
387
+ countryCode: 1,
388
+ username: 'user',
389
+ password: 'pass',
390
+ appSchema: 'tuyaSmart',
391
+ generateWeatherAccessory: false,
392
+ weatherAPI: '',
393
+ } as TuyaPlatformCloudConfig,
394
+ local: {
395
+ devices: [],
396
+ },
397
+ };
398
+
399
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
400
+
401
+ const mockCloudDevice = {
402
+ id: 'test-device-timeout',
403
+ name: 'Timeout Device',
404
+ isVirtualDevice: jest.fn(),
405
+ } as any;
406
+
407
+ // Should not throw, but handle gracefully
408
+ await expect(deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local)).resolves.toEqual({"devices": []});
409
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
410
+
411
+ // Local devices should remain empty (timeout treated as error)
412
+ if (enrichedLocalConfig?.devices) {
413
+ expect(enrichedLocalConfig.devices.length).toBe(0);
414
+ }
415
+ });
416
+ });
417
+
418
+ describe('mixed config scenarios', () => {
419
+ test('updates manual local config with cloud-provided local_key', async () => {
420
+ const bothConfig = {
421
+ platform: 'TuyaPlatform',
422
+ name: 'Tuya',
423
+ mode: 'both',
424
+ cloud: {
425
+ projectType: '2',
426
+ accessId: 'id',
427
+ accessKey: 'key',
428
+ countryCode: 1,
429
+ username: 'user',
430
+ password: 'pass',
431
+ appSchema: 'tuyaSmart',
432
+ generateWeatherAccessory: false,
433
+ weatherAPI: '',
434
+ } as TuyaPlatformCloudConfig,
435
+ local: {
436
+ devices: [
437
+ {
438
+ tuyaDeviceId: 'manual-device-xyz',
439
+ // User provided device ID but no key
440
+ name: 'Manual Device No Key',
441
+ },
442
+ ],
443
+ },
444
+ };
445
+
446
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
447
+
448
+ const mockCloudDevice = {
449
+ id: 'manual-device-xyz',
450
+ name: 'Manual Device No Key',
451
+ local_key: 'cloud-filled-key',
452
+ isVirtualDevice: jest.fn(),
453
+ } as any;
454
+
455
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
456
+
457
+ if (enrichedLocalConfig?.devices) {
458
+ const enrichedDevice = enrichedLocalConfig.devices[0];
459
+ expect(enrichedDevice.tuyaDeviceId).toBe('manual-device-xyz');
460
+ expect(enrichedDevice.tuyaKey).toBe('cloud-filled-key');
461
+ // Original name should be preserved
462
+ expect(enrichedDevice.name).toBe('Manual Device No Key');
463
+ }
464
+ });
465
+
466
+ test('does not overwrite existing manual local_key', async () => {
467
+ const bothConfig = {
468
+ platform: 'TuyaPlatform',
469
+ name: 'Tuya',
470
+ mode: 'both',
471
+ cloud: {
472
+ projectType: '2',
473
+ accessId: 'id',
474
+ accessKey: 'key',
475
+ countryCode: 1,
476
+ username: 'user',
477
+ password: 'pass',
478
+ appSchema: 'tuyaSmart',
479
+ generateWeatherAccessory: false,
480
+ weatherAPI: '',
481
+ } as TuyaPlatformCloudConfig,
482
+ local: {
483
+ devices: [
484
+ {
485
+ tuyaDeviceId: 'device-with-manual-key',
486
+ tuyaKey: 'user-provided-key-123', // User explicitly set this
487
+ name: 'Device With Manual Key',
488
+ },
489
+ ],
490
+ },
491
+ };
492
+
493
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
494
+
495
+ const mockCloudDevice = {
496
+ id: 'device-with-manual-key',
497
+ name: 'Device With Manual Key',
498
+ isVirtualDevice: jest.fn(),
499
+ } as any;
500
+
501
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud']([mockCloudDevice], bothConfig.local);
502
+
503
+ if (enrichedLocalConfig?.devices) {
504
+ const enrichedDevice = enrichedLocalConfig.devices[0];
505
+ // Manual key should be skipped (device has tuyaKey already)
506
+ expect(enrichedDevice.tuyaKey).toBe('user-provided-key-123');
507
+ }
508
+ });
509
+ });
510
+
511
+ describe('multi-device enrichment', () => {
512
+ test('enriches multiple cloud devices in parallel', async () => {
513
+ const bothConfig = {
514
+ platform: 'TuyaPlatform',
515
+ name: 'Tuya',
516
+ mode: 'both',
517
+ cloud: {
518
+ projectType: '2',
519
+ accessId: 'id',
520
+ accessKey: 'key',
521
+ countryCode: 1,
522
+ username: 'user',
523
+ password: 'pass',
524
+ appSchema: 'tuyaSmart',
525
+ generateWeatherAccessory: false,
526
+ weatherAPI: '',
527
+ } as TuyaPlatformCloudConfig,
528
+ local: {
529
+ devices: [],
530
+ },
531
+ };
532
+
533
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
534
+
535
+ const mockCloudDevices = [
536
+ { id: 'device-1', uuid: 'device-1', name: 'Device 1', product_id: 'prod-1', local_key: 'local-key-1', isVirtualDevice: jest.fn() },
537
+ { id: 'device-2', uuid: 'device-2', name: 'Device 2', product_id: 'prod-2', local_key: 'local-key-2', isVirtualDevice: jest.fn() },
538
+ { id: 'device-3', uuid: 'device-3', name: 'Device 3', product_id: 'prod-3', local_key: 'local-key-3', isVirtualDevice: jest.fn() },
539
+ ] as any;
540
+
541
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud'](mockCloudDevices, bothConfig.local);
542
+
543
+ if (enrichedLocalConfig?.devices) {
544
+ expect(enrichedLocalConfig.devices.length).toBe(3);
545
+
546
+ const device1 = enrichedLocalConfig.devices.find(d => d.tuyaDeviceId === 'device-1');
547
+ const device2 = enrichedLocalConfig.devices.find(d => d.tuyaDeviceId === 'device-2');
548
+ const device3 = enrichedLocalConfig.devices.find(d => d.tuyaDeviceId === 'device-3');
549
+
550
+ expect(device1?.tuyaKey).toBe('local-key-1');
551
+ expect(device2?.tuyaKey).toBe('local-key-2');
552
+ expect(device3?.tuyaKey).toBe('local-key-3');
553
+ }
554
+ });
555
+
556
+ test('continues enrichment if single device fails', async () => {
557
+ const bothConfig = {
558
+ platform: 'TuyaPlatform',
559
+ name: 'Tuya',
560
+ mode: 'both',
561
+ cloud: {
562
+ projectType: '2',
563
+ accessId: 'id',
564
+ accessKey: 'key',
565
+ countryCode: 1,
566
+ username: 'user',
567
+ password: 'pass',
568
+ appSchema: 'tuyaSmart',
569
+ generateWeatherAccessory: false,
570
+ weatherAPI: '',
571
+ } as TuyaPlatformCloudConfig,
572
+ local: {
573
+ devices: [],
574
+ },
575
+ };
576
+
577
+ const deviceManager = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local, false);
578
+
579
+ const mockCloudDevices = [
580
+ { id: 'device-ok-1', uuid: 'device-ok-1', name: 'OK Device 1', local_key: 'local-key-1', isVirtualDevice: jest.fn() },
581
+ { id: 'device-fail', uuid: 'device-fail', name: 'Failing Device', local_key: 'local-key-fail' },
582
+ { id: 'device-ok-2', uuid: 'device-ok-2', name: 'OK Device 2', local_key: 'local-key-2', isVirtualDevice: jest.fn() },
583
+ ] as any;
584
+
585
+ const enrichedLocalConfig = await deviceManager['enrichLocalConfigFromCloud'](mockCloudDevices, bothConfig.local);
586
+
587
+ if (enrichedLocalConfig?.devices) {
588
+ // Should have 2 devices (one failed, skipped)
589
+ expect(enrichedLocalConfig.devices.length).toBe(2);
590
+
591
+ const hasOk1 = enrichedLocalConfig.devices.some(d => d.tuyaDeviceId === 'device-ok-1');
592
+ const hasOk2 = enrichedLocalConfig.devices.some(d => d.tuyaDeviceId === 'device-ok-2');
593
+ const hasFailed = enrichedLocalConfig.devices.some(d => d.tuyaDeviceId === 'device-fail');
594
+
595
+ expect(hasOk1).toBe(true);
596
+ expect(hasOk2).toBe(true);
597
+ expect(hasFailed).toBe(false);
598
+ }
599
+ });
600
+ });
601
+
602
+ describe('pullDevices()', () => {
603
+ let instance: TuyaHybridDeviceManager;
604
+ let cloudDeviceManager: any;
605
+ let localDeviceManager: any;
606
+
607
+ const bothConfig = {
608
+ platform: 'TuyaPlatform',
609
+ name: 'Tuya',
610
+ mode: 'both',
611
+ cloud: {
612
+ projectType: '2',
613
+ accessId: 'id',
614
+ accessKey: 'key',
615
+ countryCode: 1,
616
+ username: 'user',
617
+ password: 'pass',
618
+ appSchema: 'tuyaSmart',
619
+ generateWeatherAccessory: false,
620
+ weatherAPI: '',
621
+ } as TuyaPlatformCloudConfig,
622
+ local: {
623
+ devices: [
624
+ {
625
+ tuyaDeviceId: 'device-with-manual-key',
626
+ tuyaKey: 'user-provided-key-123', // User explicitly set this
627
+ name: 'Device With Manual Key',
628
+ },
629
+ ],
630
+ },
631
+ };
632
+
633
+ beforeEach(() => {
634
+ cloudDeviceManager = {
635
+ pullDevices: jest.fn(),
636
+ };
637
+ localDeviceManager = {
638
+ pullDevices: jest.fn(),
639
+ };
640
+
641
+ instance = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local);
642
+ instance['cloudDeviceManager'] = cloudDeviceManager;
643
+ instance['localDeviceManager'] = localDeviceManager;
644
+ });
645
+
646
+ // ---------------------------------------------------------
647
+ // 1. Both cloud and local return devices → should merge correctly
648
+ // ---------------------------------------------------------
649
+ test('should merge cloud and local devices without duplicates', async () => {
650
+ const localDevices = [
651
+ { id: 'A', name: 'Local A' },
652
+ { id: 'B', name: 'Local B' },
653
+ ];
654
+ const cloudDevices = [
655
+ { id: 'B', name: 'Cloud B' }, // duplicate
656
+ { id: 'C', name: 'Cloud C' },
657
+ ];
658
+
659
+ localDeviceManager.pullDevices.mockResolvedValue(localDevices);
660
+ cloudDeviceManager.pullDevices.mockResolvedValue(cloudDevices);
661
+
662
+ const result = await instance.pullDevices();
663
+
664
+ expect(result).toEqual([
665
+ { id: 'A', name: 'Local A' },
666
+ { id: 'B', name: 'Local B' }, // local takes precedence
667
+ { id: 'C', name: 'Cloud C' },
668
+ ]);
669
+
670
+ });
671
+
672
+ // ---------------------------------------------------------
673
+ // 2. Cloud returns empty → enrichLocalConfigFromCloud should not be called
674
+ // ---------------------------------------------------------
675
+ test('should not enrich local config when cloud devices are empty', async () => {
676
+ localDeviceManager.pullDevices.mockResolvedValue([{ id: 'A' }]);
677
+ cloudDeviceManager.pullDevices.mockResolvedValue([]);
678
+
679
+ const spyEnrichLocalConfigFromCloud = jest.fn();
680
+ (instance as any).enrichLocalConfigFromCloud = spyEnrichLocalConfigFromCloud;
681
+ const result = await instance.pullDevices();
682
+
683
+ expect(result).toEqual([{ id: 'A' }]);
684
+ expect(spyEnrichLocalConfigFromCloud).not.toHaveBeenCalled();
685
+ });
686
+
687
+ // ---------------------------------------------------------
688
+ // 3. Local returns empty → should return only cloud devices
689
+ // ---------------------------------------------------------
690
+ test('should return only cloud devices when local devices are empty', async () => {
691
+ localDeviceManager.pullDevices.mockResolvedValue([]);
692
+ cloudDeviceManager.pullDevices.mockResolvedValue([
693
+ { id: 'X', name: 'Cloud X' },
694
+ ]);
695
+
696
+ const spyEnrichLocalConfigFromCloud = jest.fn();
697
+ (instance as any).enrichLocalConfigFromCloud = spyEnrichLocalConfigFromCloud;
698
+ const result = await instance.pullDevices();
699
+
700
+ expect(result).toEqual([{ id: 'X', name: 'Cloud X' }]);
701
+ expect(spyEnrichLocalConfigFromCloud).toHaveBeenCalled();
702
+ });
703
+
704
+ // ---------------------------------------------------------
705
+ // 4. Both cloud and local return empty → should return empty array
706
+ // ---------------------------------------------------------
707
+ test('should return empty array when both sources return empty', async () => {
708
+ localDeviceManager.pullDevices.mockResolvedValue([]);
709
+ cloudDeviceManager.pullDevices.mockResolvedValue([]);
710
+
711
+ const result = await instance.pullDevices();
712
+
713
+ expect(result).toEqual([]);
714
+ });
715
+ });
716
+
717
+
718
+ describe('createDeviceConfigHash()', () => {
719
+ let instance: TuyaHybridDeviceManager;
720
+ let cloudDeviceManager: any;
721
+ let localDeviceManager: any;
722
+
723
+ const bothConfig = {
724
+ platform: 'TuyaPlatform',
725
+ name: 'Tuya',
726
+ mode: 'both',
727
+ cloud: {
728
+ projectType: '2',
729
+ accessId: 'id',
730
+ accessKey: 'key',
731
+ countryCode: 1,
732
+ username: 'user',
733
+ password: 'pass',
734
+ appSchema: 'tuyaSmart',
735
+ generateWeatherAccessory: false,
736
+ weatherAPI: '',
737
+ } as TuyaPlatformCloudConfig,
738
+ local: {
739
+ devices: [
740
+ {
741
+ tuyaDeviceId: 'device-with-manual-key',
742
+ tuyaKey: 'user-provided-key-123', // User explicitly set this
743
+ name: 'Device With Manual Key',
744
+ },
745
+ ],
746
+ },
747
+ };
748
+
749
+ beforeEach(() => {
750
+ cloudDeviceManager = {
751
+ createDeviceConfigHash: jest.fn(),
752
+ };
753
+ localDeviceManager = {
754
+ createDeviceConfigHash: jest.fn(),
755
+ };
756
+
757
+ instance = new TuyaHybridDeviceManager(mockAPI, bothConfig.cloud, bothConfig.local);
758
+ instance['cloudDeviceManager'] = cloudDeviceManager;
759
+ instance['localDeviceManager'] = localDeviceManager;
760
+ });
761
+
762
+ // ---------------------------------------------------------
763
+ // Should concatenate local and cloud hash strings
764
+ // ---------------------------------------------------------
765
+ it('should return concatenated hash from local and cloud managers', () => {
766
+ const device = { id: 'A', name: 'Device A' };
767
+
768
+ localDeviceManager.createDeviceConfigHash.mockReturnValue('localHash');
769
+ cloudDeviceManager.createDeviceConfigHash.mockReturnValue('cloudHash');
770
+
771
+ const result = instance.createDeviceConfigHash(device as any);
772
+
773
+ expect(result).toBe('localHashcloudHash');
774
+
775
+ expect(localDeviceManager.createDeviceConfigHash).toHaveBeenCalledWith(device);
776
+ expect(cloudDeviceManager.createDeviceConfigHash).toHaveBeenCalledWith(device);
777
+ });
778
+
779
+ // ---------------------------------------------------------
780
+ // Should work even if one side returns an empty string
781
+ // ---------------------------------------------------------
782
+ it('should handle empty hash values', () => {
783
+ const device = { id: 'B' };
784
+
785
+ localDeviceManager.createDeviceConfigHash.mockReturnValue('');
786
+ cloudDeviceManager.createDeviceConfigHash.mockReturnValue('cloudOnly');
787
+
788
+ const result = instance.createDeviceConfigHash(device as any);
789
+
790
+ expect(result).toBe('cloudOnly');
791
+ });
792
+
793
+ // ---------------------------------------------------------
794
+ // Should work even if both return empty strings
795
+ // ---------------------------------------------------------
796
+ it('should return empty string when both hashes are empty', () => {
797
+ const device = { id: 'C' };
798
+
799
+ localDeviceManager.createDeviceConfigHash.mockReturnValue('');
800
+ cloudDeviceManager.createDeviceConfigHash.mockReturnValue('');
801
+
802
+ const result = instance.createDeviceConfigHash(device as any);
803
+
804
+ expect(result).toBe('');
805
+ });
806
+ });
807
+
808
+ });