@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,742 @@
1
+ import { describe, expect, test, beforeEach, jest } from '@jest/globals';
2
+ import AirConditionerAccessory from '../src/shared/accessory/AirConditionerAccessory';
3
+ import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceStatus, TuyaDeviceSchemaMode, TuyaDeviceSchemaType } from '../src/cloud/device/TuyaDevice';
4
+ import { ExLogger, initLogger } from '../src/shared/util/Logger';
5
+
6
+ // Mock Logger
7
+ const mockLog: ExLogger = {
8
+ debug: jest.fn(),
9
+ info: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ log: jest.fn(),
13
+ success: jest.fn(),
14
+ } as unknown as ExLogger;
15
+
16
+ // Mock data
17
+ const mockSchema = (code: string, type: string, mode: string, property: any): TuyaDeviceSchema => ({
18
+ code,
19
+ mode: mode as TuyaDeviceSchemaMode,
20
+ type: type as TuyaDeviceSchemaType,
21
+ property,
22
+ });
23
+
24
+ const mockStatus = (code: string, value: string | number | boolean): TuyaDeviceStatus => ({
25
+ code,
26
+ value,
27
+ });
28
+
29
+ // Create mock device with AC capabilities
30
+ const createMockDevice = (overrides?: Partial<TuyaDevice>): TuyaDevice => {
31
+ const device = new TuyaDevice({
32
+ id: 'test-device-001',
33
+ uuid: 'uuid-001',
34
+ name: 'Test Air Conditioner',
35
+ online: true,
36
+ owner_id: 'home-001',
37
+ product_id: 'ac-product-001',
38
+ product_name: 'Smart Air Conditioner',
39
+ model: 'AC-2024',
40
+ icon: 'ac.png',
41
+ category: 'ac',
42
+ schema: [
43
+ mockSchema('switch', 'Boolean', 'rw', {}),
44
+ mockSchema('mode', 'Enum', 'rw', { range: ['auto', 'cold', 'hot', 'wet', 'wind'] }),
45
+ mockSchema('work_status', 'Enum', 'ro', { range: ['heating', 'cooling', 'idle'] }),
46
+ mockSchema('temp_current', 'Integer', 'ro', { min: 0, max: 100, scale: 0, step: 1, unit: '°C' }),
47
+ mockSchema('temp_set', 'Integer', 'rw', { min: 16, max: 30, scale: 0, step: 1, unit: '°C' }),
48
+ mockSchema('fan_speed_enum', 'Enum', 'rw', { range: ['auto', 'low', 'middle', 'high'] }),
49
+ mockSchema('lock', 'Boolean', 'rw', {}),
50
+ mockSchema('humidity_current', 'Integer', 'ro', { min: 0, max: 100, scale: 0, step: 1, unit: '%' }),
51
+ mockSchema('humidity_set', 'Integer', 'rw', { min: 30, max: 80, scale: 0, step: 1, unit: '%' }),
52
+ mockSchema('temp_unit_convert', 'Enum', 'rw', { range: ['C', 'F'] }),
53
+ ],
54
+ status: [
55
+ mockStatus('switch', true),
56
+ mockStatus('mode', 'cold'),
57
+ mockStatus('work_status', 'cooling'),
58
+ mockStatus('temp_current', 24),
59
+ mockStatus('temp_set', 22),
60
+ mockStatus('fan_speed_enum', 'auto'),
61
+ mockStatus('lock', false),
62
+ mockStatus('humidity_current', 60),
63
+ mockStatus('humidity_set', 60),
64
+ mockStatus('temp_unit_convert', 'C'),
65
+ ],
66
+ sub: false,
67
+ ...overrides,
68
+ });
69
+ return device;
70
+ };
71
+
72
+ // Create mock platform
73
+ const createMockPlatform = (): any => {
74
+ // Mock the HAP objects needed by the accessory
75
+ const MockService = {
76
+ HeaterCooler: 'HeaterCooler',
77
+ HumidifierDehumidifier: 'HumidifierDehumidifier',
78
+ Fanv2: 'Fanv2',
79
+ AccessoryInformation: 'AccessoryInformation',
80
+ Battery: 'Battery',
81
+ };
82
+
83
+ const MockCharacteristic = {
84
+ Active: { ACTIVE: 1, INACTIVE: 0 },
85
+ CurrentHeaterCoolerState: { INACTIVE: 0, HEATING: 1, COOLING: 2 },
86
+ TargetHeaterCoolerState: { AUTO: 0, HEAT: 1, COOL: 2 },
87
+ CurrentHumidifierDehumidifierState: { DEHUMIDIFYING: 2 },
88
+ TargetHumidifierDehumidifierState: { DEHUMIDIFIER: 2 },
89
+ CurrentRelativeHumidity: 'CurrentRelativeHumidity',
90
+ RelativeHumidityDehumidifierThreshold: 'RelativeHumidityDehumidifierThreshold',
91
+ CurrentTemperature: 'CurrentTemperature',
92
+ CoolingThresholdTemperature: 'CoolingThresholdTemperature',
93
+ HeatingThresholdTemperature: 'HeatingThresholdTemperature',
94
+ RotationSpeed: 'RotationSpeed',
95
+ LockPhysicalControls: 'LockPhysicalControls',
96
+ Manufacturer: 'Manufacturer',
97
+ Model: 'Model',
98
+ Name: 'Name',
99
+ ConfiguredName: 'ConfiguredName',
100
+ SerialNumber: 'SerialNumber',
101
+ StatusLowBattery: { BATTERY_LEVEL_NORMAL: 0, BATTERY_LEVEL_LOW: 1 },
102
+ BatteryLevel: 'BatteryLevel',
103
+ ChargingState: { NOT_CHARGING: 0, CHARGING: 1 },
104
+ StatusActive: 'StatusActive',
105
+ ProgrammableSwitchEvent: { UUID: 'ProgrammableSwitchEvent' },
106
+ TemperatureDisplayUnits: { CELSIUS: 0, FAHRENHEIT: 1 },
107
+ };
108
+
109
+ return {
110
+ api: {
111
+ hap: {
112
+ Service: MockService,
113
+ Characteristic: MockCharacteristic,
114
+ HapStatusError: class HapStatusError extends Error {
115
+ constructor(public status: number) {
116
+ super();
117
+ }
118
+ },
119
+ HAPStatus: {
120
+ SERVICE_COMMUNICATION_FAILURE: 72,
121
+ },
122
+ },
123
+ },
124
+ deviceManager: {
125
+ getDevice: jest.fn(),
126
+ getDeviceSchemaConfig: jest.fn(),
127
+ sendCommands: jest.fn(),
128
+ },
129
+ options: {
130
+ debug: false,
131
+ debugLevel: '',
132
+ }
133
+ };
134
+ };
135
+
136
+ // Create mock accessory
137
+ const createMockAccessory = (): any => ({
138
+ context: { deviceID: 'test-device-001' },
139
+ displayName: 'Test Air Conditioner',
140
+ services: [],
141
+ getService: jest.fn(),
142
+ addService: jest.fn(),
143
+ });
144
+
145
+ const createMockCharacteristic = () => {
146
+ const characteristic: any = {
147
+ getHandler: undefined as any,
148
+ setHandler: undefined as any,
149
+ onGet: jest.fn().mockImplementation(function (fn: any) {
150
+ characteristic.getHandler = fn;
151
+ return characteristic;
152
+ }),
153
+ onSet: jest.fn().mockImplementation(function (fn: any) {
154
+ characteristic.setHandler = fn;
155
+ return characteristic;
156
+ }),
157
+ updateValue: jest.fn().mockReturnThis(),
158
+ setProps: jest.fn().mockReturnThis(),
159
+ };
160
+ return characteristic;
161
+ };
162
+
163
+ const createMockService = () => {
164
+ const characteristics = new Map<string, any>();
165
+ const getCharacteristic = jest.fn((key: any) => {
166
+ const keyName = typeof key === 'string' ? key : JSON.stringify(key);
167
+ if (!characteristics.has(keyName)) {
168
+ characteristics.set(keyName, createMockCharacteristic());
169
+ }
170
+ return characteristics.get(keyName);
171
+ });
172
+ return {
173
+ getCharacteristic,
174
+ setCharacteristic: jest.fn().mockReturnThis(),
175
+ addOptionalCharacteristic: jest.fn(),
176
+ testCharacteristic: jest.fn(() => false),
177
+ characteristics,
178
+ };
179
+ };
180
+
181
+ describe('AirConditionerAccessory', () => {
182
+ let platform: any;
183
+ let accessory: any;
184
+ let device: TuyaDevice;
185
+ let airConditionerAccessory: AirConditionerAccessory;
186
+
187
+ beforeEach(() => {
188
+ initLogger(mockLog);
189
+ platform = createMockPlatform();
190
+ accessory = createMockAccessory();
191
+ device = createMockDevice();
192
+
193
+ platform.deviceManager.getDevice.mockReturnValue(device);
194
+ platform.deviceManager.getDeviceSchemaConfig.mockReturnValue(undefined);
195
+
196
+ // Setup mock services and characteristics
197
+ const mockHeaterCoolerService = {
198
+ getCharacteristic: jest.fn(),
199
+ setCharacteristic: jest.fn().mockReturnThis(),
200
+ addOptionalCharacteristic: jest.fn(),
201
+ testCharacteristic: jest.fn(() => false),
202
+ };
203
+
204
+ const mockHumidifierService = {
205
+ getCharacteristic: jest.fn(),
206
+ setCharacteristic: jest.fn().mockReturnThis(),
207
+ addOptionalCharacteristic: jest.fn(),
208
+ testCharacteristic: jest.fn(() => false),
209
+ };
210
+
211
+ const mockFanService = {
212
+ getCharacteristic: jest.fn(),
213
+ setCharacteristic: jest.fn().mockReturnThis(),
214
+ addOptionalCharacteristic: jest.fn(),
215
+ testCharacteristic: jest.fn(() => false),
216
+ };
217
+
218
+ const mockAccessoryInformationService = {
219
+ setCharacteristic: jest.fn().mockReturnThis(),
220
+ getCharacteristic: jest.fn(),
221
+ };
222
+
223
+ const mockBatteryService = {
224
+ getCharacteristic: jest.fn(),
225
+ setCharacteristic: jest.fn().mockReturnThis(),
226
+ };
227
+
228
+ // Create a service map for easier mocking
229
+ const serviceMap: any = {
230
+ 'HeaterCooler': mockHeaterCoolerService,
231
+ 'HumidifierDehumidifier': mockHumidifierService,
232
+ 'Fanv2': mockFanService,
233
+ 'AccessoryInformation': mockAccessoryInformationService,
234
+ 'Battery': mockBatteryService,
235
+ };
236
+
237
+ // Both getService and addService should return the same mock services
238
+ accessory.getService.mockImplementation((serviceType: any) => {
239
+ const serviceName = typeof serviceType === 'string' ? serviceType : serviceType?.UUID;
240
+ return serviceMap[serviceName] || mockAccessoryInformationService; // Default to AccessoryInformation
241
+ });
242
+
243
+ accessory.addService.mockImplementation((serviceType: any, ...args: any[]) => {
244
+ const serviceName = typeof serviceType === 'string' ? serviceType : serviceType?.UUID;
245
+ return serviceMap[serviceName] || mockAccessoryInformationService; // Default to AccessoryInformation
246
+ });
247
+
248
+ // Mock characteristic getters
249
+ const mockCharacteristic = {
250
+ onGet: jest.fn().mockReturnThis(),
251
+ onSet: jest.fn().mockReturnThis(),
252
+ updateValue: jest.fn().mockReturnThis(),
253
+ setProps: jest.fn().mockReturnThis(),
254
+ };
255
+
256
+ mockHeaterCoolerService.getCharacteristic.mockReturnValue(mockCharacteristic);
257
+ mockHumidifierService.getCharacteristic.mockReturnValue(mockCharacteristic);
258
+ mockFanService.getCharacteristic.mockReturnValue(mockCharacteristic);
259
+ mockAccessoryInformationService.getCharacteristic.mockReturnValue(mockCharacteristic);
260
+ mockBatteryService.getCharacteristic.mockReturnValue(mockCharacteristic);
261
+
262
+ // Store the service map for testing
263
+ (accessory as any).serviceMap = serviceMap;
264
+
265
+ airConditionerAccessory = new AirConditionerAccessory(platform, accessory);
266
+ });
267
+
268
+ describe('requiredSchema', () => {
269
+ test('should return required schema codes for AC', () => {
270
+ const required = airConditionerAccessory.requiredSchema();
271
+ expect(required).toBeDefined();
272
+ expect(Array.isArray(required)).toBe(true);
273
+ expect(required.length).toBeGreaterThan(0);
274
+ });
275
+
276
+ test('should include switch, mode, work_status, and temp_current', () => {
277
+ const required = airConditionerAccessory.requiredSchema();
278
+ expect(required).toContainEqual(['switch']);
279
+ expect(required).toContainEqual(['mode']);
280
+ });
281
+ });
282
+
283
+ describe('configureServices', () => {
284
+ test('should configure all three services (AC, Dehumidifier, Fan)', () => {
285
+ // Reset the mock to track new calls during configureServices
286
+ jest.clearAllMocks();
287
+
288
+ airConditionerAccessory.configureServices();
289
+
290
+ // Check that services are being configured (getService/addService will be called)
291
+ expect(accessory.getService).toHaveBeenCalled();
292
+ });
293
+
294
+ test('should configure main HeaterCooler service', () => {
295
+ airConditionerAccessory.configureServices();
296
+
297
+ const mainService = accessory.getService('HeaterCooler');
298
+ expect(mainService).toBeDefined();
299
+ });
300
+ });
301
+
302
+ describe('configureAirConditioner', () => {
303
+ test('should setup Active characteristic handlers', () => {
304
+ const mockService = {
305
+ getCharacteristic: jest.fn(),
306
+ setCharacteristic: jest.fn(),
307
+ };
308
+
309
+ jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(mockService as any);
310
+
311
+ const mockCharacteristic = {
312
+ onGet: jest.fn().mockReturnThis(),
313
+ onSet: jest.fn().mockReturnThis(),
314
+ updateValue: jest.fn().mockReturnThis(),
315
+ setProps: jest.fn().mockReturnThis(),
316
+ };
317
+
318
+ mockService.getCharacteristic.mockReturnValue(mockCharacteristic);
319
+
320
+ airConditionerAccessory.configureAirConditioner();
321
+
322
+ expect(mockService.getCharacteristic).toHaveBeenCalled();
323
+ });
324
+
325
+ test('should handle Active get returns ACTIVE when device is on and in AC mode', async () => {
326
+ device.status = [
327
+ mockStatus('switch', true),
328
+ mockStatus('mode', 'cold'),
329
+ mockStatus('work_status', 'cooling'),
330
+ mockStatus('temp_current', 24),
331
+ mockStatus('temp_set', 22),
332
+ mockStatus('fan_speed_enum', 'auto'),
333
+ mockStatus('lock', false),
334
+ mockStatus('humidity_current', 60),
335
+ mockStatus('humidity_set', 60),
336
+ mockStatus('temp_unit_convert', 'C'),
337
+ ];
338
+
339
+ const activeStatus = airConditionerAccessory.getStatus('switch');
340
+ const modeStatus = airConditionerAccessory.getStatus('mode');
341
+
342
+ expect(activeStatus?.value).toBe(true);
343
+ expect(modeStatus?.value).toBe('cold');
344
+ });
345
+
346
+ test('should handle Active get returns INACTIVE when device is off', () => {
347
+ device.status[0] = mockStatus('switch', false); // Turn off
348
+
349
+ const activeStatus = airConditionerAccessory.getStatus('switch');
350
+ expect(activeStatus?.value).toBe(false);
351
+ });
352
+ });
353
+
354
+ describe('configureDehumidifier', () => {
355
+ test('should configure dehumidifier service when mode includes wet', () => {
356
+ const mockService = {
357
+ getCharacteristic: jest.fn(),
358
+ setCharacteristic: jest.fn(),
359
+ };
360
+
361
+ jest.spyOn(airConditionerAccessory, 'dehumidifierService').mockReturnValue(mockService as any);
362
+
363
+ const mockCharacteristic = {
364
+ onGet: jest.fn().mockReturnThis(),
365
+ onSet: jest.fn().mockReturnThis(),
366
+ updateValue: jest.fn().mockReturnThis(),
367
+ setProps: jest.fn().mockReturnThis(),
368
+ };
369
+
370
+ mockService.getCharacteristic.mockReturnValue(mockCharacteristic);
371
+
372
+ airConditionerAccessory.configureDehumidifier();
373
+
374
+ expect(mockService.getCharacteristic).toHaveBeenCalled();
375
+ });
376
+
377
+ test('should not configure dehumidifier when mode does not include wet', () => {
378
+ const deviceWithoutWet = createMockDevice({
379
+ schema: [
380
+ mockSchema('switch', 'Boolean', 'rw', {}),
381
+ mockSchema('mode', 'Enum', 'rw', { range: ['auto', 'cold', 'hot'] }),
382
+ ],
383
+ });
384
+ platform.deviceManager.getDevice.mockReturnValue(deviceWithoutWet);
385
+
386
+ const ac = new AirConditionerAccessory(platform, accessory);
387
+ const mockService = {
388
+ getCharacteristic: jest.fn(),
389
+ setCharacteristic: jest.fn(),
390
+ };
391
+
392
+ jest.spyOn(ac, 'dehumidifierService').mockReturnValue(mockService as any);
393
+
394
+ ac.configureDehumidifier();
395
+
396
+ // Should not configure if wet mode is not available
397
+ expect(mockService.getCharacteristic).not.toHaveBeenCalled();
398
+ });
399
+ });
400
+
401
+ describe('configureFan', () => {
402
+ test('should configure fan service when mode includes wind', () => {
403
+ const mockService = {
404
+ getCharacteristic: jest.fn(),
405
+ setCharacteristic: jest.fn(),
406
+ };
407
+
408
+ jest.spyOn(airConditionerAccessory, 'fanService').mockReturnValue(mockService as any);
409
+
410
+ const mockCharacteristic = {
411
+ onGet: jest.fn().mockReturnThis(),
412
+ onSet: jest.fn().mockReturnThis(),
413
+ updateValue: jest.fn().mockReturnThis(),
414
+ setProps: jest.fn().mockReturnThis(),
415
+ };
416
+
417
+ mockService.getCharacteristic.mockReturnValue(mockCharacteristic);
418
+
419
+ airConditionerAccessory.configureFan();
420
+
421
+ expect(mockService.getCharacteristic).toHaveBeenCalled();
422
+ });
423
+
424
+ test('should not configure fan when mode does not include wind', () => {
425
+ const deviceWithoutWind = createMockDevice({
426
+ schema: [
427
+ mockSchema('switch', 'Boolean', 'rw', {}),
428
+ mockSchema('mode', 'Enum', 'rw', { range: ['auto', 'cold', 'hot'] }),
429
+ ],
430
+ });
431
+ platform.deviceManager.getDevice.mockReturnValue(deviceWithoutWind);
432
+
433
+ const ac = new AirConditionerAccessory(platform, accessory);
434
+ const mockService = {
435
+ getCharacteristic: jest.fn(),
436
+ setCharacteristic: jest.fn(),
437
+ };
438
+
439
+ jest.spyOn(ac, 'fanService').mockReturnValue(mockService as any);
440
+
441
+ ac.configureFan();
442
+
443
+ // Should not configure if wind mode is not available
444
+ expect(mockService.getCharacteristic).not.toHaveBeenCalled();
445
+ });
446
+ });
447
+
448
+ describe('Characteristic handlers', () => {
449
+ test('HeaterCooler Active onGet/onSet should reflect AC active state and send commands', async () => {
450
+ const service = createMockService();
451
+ jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
452
+
453
+ airConditionerAccessory.configureAirConditioner();
454
+
455
+ const activeChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.Active));
456
+ expect(activeChar).toBeDefined();
457
+ expect(activeChar.getHandler).toBeDefined();
458
+ expect(activeChar.setHandler).toBeDefined();
459
+
460
+ const activeValue = activeChar.getHandler();
461
+ expect(activeValue).toBe(platform.api.hap.Characteristic.Active.ACTIVE);
462
+
463
+ jest.useFakeTimers();
464
+ activeChar.setHandler(platform.api.hap.Characteristic.Active.ACTIVE);
465
+ jest.runAllTimers();
466
+ jest.useRealTimers();
467
+
468
+ expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [{ code: 'switch', value: true }]);
469
+ expect(airConditionerAccessory.getStatus('switch')?.value).toBe(true);
470
+ });
471
+
472
+ test('TargetHeaterCoolerState onGet/onSet should map mode values correctly', async () => {
473
+ const service = createMockService();
474
+ jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
475
+
476
+ airConditionerAccessory.configureAirConditioner();
477
+
478
+ const targetChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.TargetHeaterCoolerState));
479
+ expect(targetChar).toBeDefined();
480
+ expect(targetChar.getHandler).toBeDefined();
481
+ expect(targetChar.setHandler).toBeDefined();
482
+
483
+ device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'hot') : status);
484
+ const targetValue = targetChar.getHandler();
485
+ expect(targetValue).toBe(platform.api.hap.Characteristic.TargetHeaterCoolerState.HEAT);
486
+
487
+ jest.useFakeTimers();
488
+ targetChar.setHandler(platform.api.hap.Characteristic.TargetHeaterCoolerState.COOL);
489
+ jest.runAllTimers();
490
+ jest.useRealTimers();
491
+
492
+ expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [{ code: 'mode', value: 'cold' }]);
493
+ expect(airConditionerAccessory.getStatus('mode')?.value).toBe('cold');
494
+ });
495
+
496
+ test('CoolingThresholdTemperature onGet/onSet should respect auto mode limits', async () => {
497
+ const service = createMockService();
498
+ jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
499
+ airConditionerAccessory.configureAirConditioner();
500
+
501
+ const temperatureKey = platform.api.hap.Characteristic.CoolingThresholdTemperature;
502
+ const coolingChar = service.getCharacteristic(temperatureKey);
503
+ expect(coolingChar).toBeDefined();
504
+ expect(coolingChar.getHandler).toBeDefined();
505
+ expect(coolingChar.setHandler).toBeDefined();
506
+
507
+ device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'auto') : status);
508
+ const getValue = coolingChar.getHandler();
509
+ expect(getValue).toBe(16);
510
+
511
+ jest.useFakeTimers();
512
+ coolingChar.setHandler(18);
513
+ jest.runAllTimers();
514
+ jest.useRealTimers();
515
+
516
+ expect(service.getCharacteristic(temperatureKey).updateValue).toHaveBeenCalledWith(16);
517
+ expect(airConditionerAccessory.getStatus('temp_set')?.value).toBe(22);
518
+ });
519
+
520
+ test('HeatingThresholdTemperature onGet/onSet should respect auto mode limits', async () => {
521
+ const service = createMockService();
522
+ jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
523
+ airConditionerAccessory.configureAirConditioner();
524
+
525
+ const temperatureKey = platform.api.hap.Characteristic.HeatingThresholdTemperature;
526
+ const heatingChar = service.getCharacteristic(temperatureKey);
527
+ expect(heatingChar).toBeDefined();
528
+ expect(heatingChar.getHandler).toBeDefined();
529
+ expect(heatingChar.setHandler).toBeDefined();
530
+
531
+ device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'auto') : status);
532
+ const getValue = heatingChar.getHandler();
533
+ expect(getValue).toBe(30);
534
+
535
+ jest.useFakeTimers();
536
+ heatingChar.setHandler(25);
537
+ jest.runAllTimers();
538
+ jest.useRealTimers();
539
+
540
+ expect(service.getCharacteristic(temperatureKey).updateValue).toHaveBeenCalledWith(30);
541
+ expect(airConditionerAccessory.getStatus('temp_set')?.value).toBe(22);
542
+ });
543
+
544
+ test('CurrentHeaterCoolerState onGet should return correct operating state', () => {
545
+ const service = createMockService();
546
+ jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
547
+ airConditionerAccessory.configureAirConditioner();
548
+
549
+ const currentStateChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.CurrentHeaterCoolerState));
550
+ expect(currentStateChar).toBeDefined();
551
+
552
+ device.status = device.status.map(status => status.code === 'work_status' ? mockStatus('work_status', 'heating') : status);
553
+ expect(currentStateChar.getHandler()).toBe(platform.api.hap.Characteristic.CurrentHeaterCoolerState.HEATING);
554
+
555
+ device.status = device.status.map(status => status.code === 'work_status' ? mockStatus('work_status', 'idle') : status);
556
+ expect(currentStateChar.getHandler()).toBe(platform.api.hap.Characteristic.CurrentHeaterCoolerState.INACTIVE);
557
+ });
558
+
559
+ test('Dehumidifier Active onGet/onSet should send wet mode commands', async () => {
560
+ const service = createMockService();
561
+ jest.spyOn(airConditionerAccessory, 'dehumidifierService').mockReturnValue(service as any);
562
+ airConditionerAccessory.configureDehumidifier();
563
+
564
+ const activeChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.Active));
565
+ expect(activeChar).toBeDefined();
566
+ expect(activeChar.getHandler).toBeDefined();
567
+ expect(activeChar.setHandler).toBeDefined();
568
+
569
+ device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'wet') : status);
570
+ const activeValue = activeChar.getHandler();
571
+ expect(activeValue).toBe(platform.api.hap.Characteristic.Active.ACTIVE);
572
+
573
+ jest.useFakeTimers();
574
+ activeChar.setHandler(platform.api.hap.Characteristic.Active.INACTIVE);
575
+ jest.runAllTimers();
576
+ jest.useRealTimers();
577
+
578
+ expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [
579
+ { code: 'switch', value: false },
580
+ { code: 'mode', value: 'wet' },
581
+ ]);
582
+ expect(airConditionerAccessory.getStatus('switch')?.value).toBe(false);
583
+ });
584
+
585
+ test('Fan Active onGet/onSet should send wind mode commands', async () => {
586
+ const service = createMockService();
587
+ jest.spyOn(airConditionerAccessory, 'fanService').mockReturnValue(service as any);
588
+ airConditionerAccessory.configureFan();
589
+
590
+ const activeChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.Active));
591
+ expect(activeChar).toBeDefined();
592
+ expect(activeChar.getHandler).toBeDefined();
593
+ expect(activeChar.setHandler).toBeDefined();
594
+
595
+ device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'wind') : status);
596
+ const activeValue = activeChar.getHandler();
597
+ expect(activeValue).toBe(platform.api.hap.Characteristic.Active.ACTIVE);
598
+
599
+ jest.useFakeTimers();
600
+ activeChar.setHandler(platform.api.hap.Characteristic.Active.ACTIVE);
601
+ jest.runAllTimers();
602
+ jest.useRealTimers();
603
+
604
+ expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [
605
+ { code: 'switch', value: true },
606
+ { code: 'mode', value: 'wind' },
607
+ ]);
608
+ expect(airConditionerAccessory.getStatus('switch')?.value).toBe(true);
609
+ });
610
+ });
611
+
612
+ describe('getSchema', () => {
613
+ test('should return schema for valid code', () => {
614
+ const schema = airConditionerAccessory.getSchema('switch');
615
+ expect(schema).toBeDefined();
616
+ expect(schema?.code).toBe('switch');
617
+ });
618
+
619
+ test('should return schema for alternative code names', () => {
620
+ // SCHEMA_CODE.FAN_SPEED_ENUM should map to 'fan_speed_enum'
621
+ const schema = airConditionerAccessory.getSchema('windspeed', 'fan_speed_enum');
622
+ expect(schema).toBeDefined();
623
+ expect(device.schema.some((s) => s.code === 'fan_speed_enum')).toBeTruthy();
624
+ expect(schema?.code).toBe('fan_speed_enum');
625
+ });
626
+
627
+ test('should return undefined for non-existent code', () => {
628
+ const schema = airConditionerAccessory.getSchema('non_existent_code');
629
+ expect(schema).toBeUndefined();
630
+ });
631
+
632
+ test('should be case-insensitive', () => {
633
+ const schema1 = airConditionerAccessory.getSchema('SWITCH');
634
+ const schema2 = airConditionerAccessory.getSchema('switch');
635
+ expect(schema1?.code).toBe(schema2?.code);
636
+ });
637
+ });
638
+
639
+ describe('getStatus', () => {
640
+ test('should return status for valid code', () => {
641
+ const status = airConditionerAccessory.getStatus('switch');
642
+ expect(status).toBeDefined();
643
+ expect(status?.code).toBe('switch');
644
+ expect(status?.value).toBe(true);
645
+ });
646
+
647
+ test('should return status for temperature', () => {
648
+ const status = airConditionerAccessory.getStatus('temp_current');
649
+ expect(status?.value).toBe(24);
650
+ });
651
+
652
+ test('should return undefined for non-existent code', () => {
653
+ const status = airConditionerAccessory.getStatus('non_existent');
654
+ expect(status).toBeUndefined();
655
+ });
656
+ });
657
+
658
+ describe('service methods', () => {
659
+ test('mainService should return HeaterCooler service', () => {
660
+ const service = airConditionerAccessory.mainService();
661
+ expect(service).toBeDefined();
662
+ });
663
+
664
+ test('dehumidifierService should return HumidifierDehumidifier service', () => {
665
+ const service = airConditionerAccessory.dehumidifierService();
666
+ expect(service).toBeDefined();
667
+ });
668
+
669
+ test('fanService should return Fanv2 service', () => {
670
+ const service = airConditionerAccessory.fanService();
671
+ expect(service).toBeDefined();
672
+ });
673
+ });
674
+
675
+ describe('device offline handling', () => {
676
+ test('should update characteristic when device status changes', () => {
677
+ device.status[0] = mockStatus('switch', false);
678
+
679
+ const status = airConditionerAccessory.getStatus('switch');
680
+ expect(status?.value).toBe(false);
681
+ });
682
+
683
+ test('should handle multiple mode values correctly', () => {
684
+ const modeSchema = airConditionerAccessory.getSchema('mode');
685
+ expect(modeSchema).toBeDefined();
686
+ const property = modeSchema?.property as any;
687
+ expect(property.range).toContain('auto');
688
+ expect(property.range).toContain('cold');
689
+ expect(property.range).toContain('hot');
690
+ });
691
+ });
692
+
693
+ describe('temperature configuration', () => {
694
+ test('should have correct temperature range', () => {
695
+ const schema = airConditionerAccessory.getSchema('temp_set');
696
+ expect(schema).toBeDefined();
697
+ const property = schema?.property as any;
698
+ expect(property.min).toBe(16);
699
+ expect(property.max).toBe(30);
700
+ });
701
+
702
+ test('should have correct humidity range', () => {
703
+ const schema = airConditionerAccessory.getSchema('humidity_set');
704
+ expect(schema).toBeDefined();
705
+ const property = schema?.property as any;
706
+ expect(property.min).toBe(30);
707
+ expect(property.max).toBe(80);
708
+ });
709
+ });
710
+
711
+ describe('mode switching', () => {
712
+ test('should support cold mode', () => {
713
+ device.status[1] = mockStatus('mode', 'cold');
714
+ const status = airConditionerAccessory.getStatus('mode');
715
+ expect(status?.value).toBe('cold');
716
+ });
717
+
718
+ test('should support hot mode', () => {
719
+ device.status[1] = mockStatus('mode', 'hot');
720
+ const status = airConditionerAccessory.getStatus('mode');
721
+ expect(status?.value).toBe('hot');
722
+ });
723
+
724
+ test('should support auto mode', () => {
725
+ device.status[1] = mockStatus('mode', 'auto');
726
+ const status = airConditionerAccessory.getStatus('mode');
727
+ expect(status?.value).toBe('auto');
728
+ });
729
+
730
+ test('should support wet (dehumidifier) mode', () => {
731
+ device.status[1] = mockStatus('mode', 'wet');
732
+ const status = airConditionerAccessory.getStatus('mode');
733
+ expect(status?.value).toBe('wet');
734
+ });
735
+
736
+ test('should support wind (fan) mode', () => {
737
+ device.status[1] = mockStatus('mode', 'wind');
738
+ const status = airConditionerAccessory.getStatus('mode');
739
+ expect(status?.value).toBe('wind');
740
+ });
741
+ });
742
+ });