@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
package/dist/platform.js CHANGED
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
@@ -40,18 +7,23 @@ exports.TuyaPlatform = void 0;
40
7
  const jsonschema_1 = require("jsonschema");
41
8
  const path_1 = __importDefault(require("path"));
42
9
  const fs_1 = __importDefault(require("fs"));
43
- const TuyaDeviceManager_1 = __importDefault(require("./cloud/device/TuyaDeviceManager"));
10
+ const uuid_1 = require("uuid");
11
+ // Cloud imports (from src/cloud/)
12
+ const TuyaDevice_1 = __importDefault(require("./cloud/device/TuyaDevice"));
44
13
  const TuyaCustomDeviceManager_1 = __importDefault(require("./cloud/device/TuyaCustomDeviceManager"));
45
14
  const TuyaHomeDeviceManager_1 = __importDefault(require("./cloud/device/TuyaHomeDeviceManager"));
46
- const TuyaOpenAPI_1 = __importStar(require("./cloud/api/TuyaOpenAPI"));
15
+ const TuyaOpenAPI_1 = __importDefault(require("./cloud/api/TuyaOpenAPI"));
47
16
  // Local imports (from src/local/)
48
17
  const LocalDeviceManager_1 = __importDefault(require("./local/LocalDeviceManager"));
49
18
  // Shared imports (from src/shared/)
50
- const AccessoryFactory_1 = __importDefault(require("./shared/accessories/AccessoryFactory"));
19
+ const TuyaDeviceManager_1 = __importDefault(require("./shared/TuyaDeviceManager"));
20
+ const ConfigMigrator_1 = __importDefault(require("./shared/util/ConfigMigrator"));
21
+ const AccessoryFactory_1 = __importDefault(require("./shared/accessory/AccessoryFactory"));
51
22
  const util_1 = require("./shared/util/util");
52
- const ConfigHash_1 = require("./shared/util/ConfigHash");
53
23
  const settings_1 = require("./settings");
54
24
  const config_1 = require("./config");
25
+ const Logger_1 = require("./shared/util/Logger");
26
+ const TuyaHybridDeviceManager_1 = __importDefault(require("./shared/TuyaHybridDeviceManager"));
55
27
  /**
56
28
  * TuyaPlatform — unified Homebridge platform supporting both Tuya Cloud (REST/MQTT)
57
29
  * and Tuya Local (direct LAN TCP) device communication.
@@ -62,9 +34,10 @@ const config_1 = require("./config");
62
34
  * "both" — cloud + local simultaneously
63
35
  */
64
36
  class TuyaPlatform {
37
+ static { this.PLATFORM_UUID = '8CC2405F-4DB0-4586-B32F-A38CC156164D'; }
65
38
  validate() {
66
39
  // Local-only mode does not need cloud options
67
- if (this.mode === 'local') {
40
+ if (this.mode === config_1.TuyaPluginMode.local) {
68
41
  if (!this.platformConfig.local) {
69
42
  this.log.error('mode is "local" but no "local" config block found.');
70
43
  return false;
@@ -72,41 +45,42 @@ class TuyaPlatform {
72
45
  return true;
73
46
  }
74
47
  // Both mode requires a local block in addition to cloud options
75
- if (this.mode === 'both' && !this.platformConfig.local) {
48
+ if (this.mode === config_1.TuyaPluginMode.both && !this.platformConfig.local) {
76
49
  this.log.error('mode is "both" but no "local" config block found.');
77
50
  return false;
78
51
  }
79
52
  // Cloud or "both" mode requires cloud options
80
53
  let result;
81
- if (!this.options) {
82
- this.log.error('Not configured — "options" block is required for cloud mode, exit.');
54
+ if (!this.platformConfig.cloud) {
55
+ this.log.error('Not configured — "cloud" block is required for cloud mode, exit.');
83
56
  return false;
84
57
  }
85
- else if (this.options.projectType === '1') {
86
- result = new jsonschema_1.Validator().validate(this.options, config_1.customOptionsSchema);
58
+ else if (this.platformConfig.cloud.projectType === '1') {
59
+ result = new jsonschema_1.Validator().validate(this.platformConfig.cloud, config_1.customOptionsSchema);
87
60
  }
88
- else if (this.options.projectType === '2') {
89
- result = new jsonschema_1.Validator().validate(this.options, config_1.homeOptionsSchema);
61
+ else if (this.platformConfig.cloud.projectType === '2') {
62
+ result = new jsonschema_1.Validator().validate(this.platformConfig.cloud, config_1.homeOptionsSchema);
90
63
  }
91
64
  else {
92
- this.log.error(`Unsupported projectType: ${this.options['projectType']}, exit.`);
65
+ this.log.error(`Unsupported projectType: ${this.platformConfig.cloud['projectType']}, exit.`);
93
66
  return false;
94
67
  }
95
68
  result.errors.forEach(error => this.log.error(error.stack));
96
69
  if (result.errors.length > 0) {
97
70
  return false;
98
71
  }
99
- if (!this.validateDeviceOverrides() || !this.validateSchema()) {
72
+ if (!this.validateDeviceOverrides(this.platformConfig.cloud.deviceOverrides)
73
+ || !this.validateSchema(this.platformConfig.cloud.deviceOverrides)) {
100
74
  return false;
101
75
  }
102
76
  return true;
103
77
  }
104
- validateDeviceOverrides() {
105
- if (!this.options.deviceOverrides) {
78
+ validateDeviceOverrides(deviceOverrides) {
79
+ if (!deviceOverrides) {
106
80
  return true;
107
81
  }
108
82
  const idMap = new Map();
109
- for (const item of this.options.deviceOverrides) {
83
+ for (const item of deviceOverrides) {
110
84
  if (idMap.has(item.id)) {
111
85
  idMap.get(item.id)?.push(item);
112
86
  }
@@ -122,11 +96,11 @@ class TuyaPlatform {
122
96
  }
123
97
  return true;
124
98
  }
125
- validateSchema() {
126
- if (!this.options.deviceOverrides) {
99
+ validateSchema(deviceOverrides) {
100
+ if (!deviceOverrides) {
127
101
  return true;
128
102
  }
129
- for (const deviceOverride of this.options.deviceOverrides) {
103
+ for (const deviceOverride of deviceOverrides) {
130
104
  if (!deviceOverride.schema) {
131
105
  continue;
132
106
  }
@@ -154,29 +128,53 @@ class TuyaPlatform {
154
128
  this.api = api;
155
129
  this.Service = this.api.hap.Service;
156
130
  this.Characteristic = this.api.hap.Characteristic;
131
+ this.platformAccessories = new Map();
132
+ /** platform ID */
133
+ this.platformID = (0, uuid_1.v5)(this.config.name ?? 'TuyaPlatform', TuyaPlatform.PLATFORM_UUID);
157
134
  /** Cast config to our typed shape for easy access. */
158
135
  this.platformConfig = this.config;
159
- /** Cloud credentials block (options.projectType, accessId, etc.) */
160
- this.options = this.config.options;
161
136
  /** Active communication mode. Defaults to "cloud" for backward compatibility. */
162
- this.mode = this.config.mode ?? 'cloud';
163
- // this is used to track restored cached accessories
164
- this.cachedAccessories = [];
137
+ this.mode = this.config.mode ?? config_1.TuyaPluginMode.cloud;
165
138
  /** All active accessory handler instances. */
166
139
  this.accessoryHandlers = [];
140
+ /** for debug */
141
+ this.debug = false;
142
+ (0, Logger_1.initLogger)(log);
143
+ this.platformConfig = new ConfigMigrator_1.default(!!this.platformConfig.common?.debug).migrate(config);
144
+ this.debug = this.platformConfig.common?.debug ?? false;
145
+ this.debugLevel = this.platformConfig.common?.debugLevel ?? undefined;
146
+ this.mode = this.platformConfig.mode;
167
147
  if (!this.validate()) {
168
148
  return;
169
149
  }
150
+ // Error/Exception handling.
151
+ const formatError = (e) => {
152
+ if (e instanceof Error) {
153
+ return `${e.message}\n${e.stack}`;
154
+ }
155
+ return JSON.stringify(e);
156
+ };
157
+ process.on('uncaughtException', (err) => {
158
+ this.log.error('Uncaught exception: %o', formatError(err));
159
+ process.exit(1);
160
+ });
161
+ process.on('unhandledRejection', (reason) => {
162
+ this.log.error('Unhandled rejection: %o', formatError(reason));
163
+ process.exit(1);
164
+ });
170
165
  this.log.debug('Finished initializing platform');
171
166
  // When this event is fired it means Homebridge has restored all cached accessories from disk.
172
167
  // Dynamic Platform plugins should only register new accessories after this event was fired,
173
168
  // in order to ensure they weren't added to homebridge already. This event can also be used
174
169
  // to start discovery of new accessories.
175
- this.api.on('didFinishLaunching', async () => {
170
+ this.api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, async () => {
176
171
  this.log.debug('Executed didFinishLaunching callback');
177
172
  // run the method to discover / register your devices as accessories
178
173
  await this.initDevices();
179
174
  });
175
+ this.api.on("shutdown" /* APIEvent.SHUTDOWN */, async () => {
176
+ this.log.debug('Executed shutdown callback');
177
+ });
180
178
  }
181
179
  /**
182
180
  * This function is invoked when homebridge restores cached accessories from disk at startup.
@@ -184,474 +182,150 @@ class TuyaPlatform {
184
182
  */
185
183
  configureAccessory(accessory) {
186
184
  this.log.info('Loading accessory from cache:', accessory.displayName);
187
- AccessoryFactory_1.default.configAccessory(this, accessory);
188
185
  // add the restored accessory to the accessories cache so we can track if it has already been registered
189
- this.cachedAccessories.push(accessory);
186
+ this.platformAccessories.set(accessory.UUID, accessory);
190
187
  }
191
- /**
192
- * This is an example method showing how to register discovered accessories.
193
- * Accessories must only be registered once, previously created accessories
194
- * must not be registered again to prevent "duplicate UUID" errors.
195
- */
196
- async initDevices() {
197
- const cloudEnabled = this.mode === 'cloud' || this.mode === 'both';
198
- const localEnabled = this.mode === 'local' || this.mode === 'both';
199
- // For "both" mode, initialize cloud FIRST to fetch device details for enrichment
200
- let cloudDevices;
201
- if (cloudEnabled && this.options && this.mode === 'both') {
202
- this.log.info('[Cloud] Initializing cloud device manager for hybrid mode enrichment…');
203
- if (this.options.projectType === '1') {
204
- cloudDevices = await this.initCustomProject();
205
- }
206
- else if (this.options.projectType === '2') {
207
- cloudDevices = await this.initHomeProject();
208
- }
209
- }
210
- // ── Local devices ────────────────────────────────────────────────────────
211
- if (localEnabled && this.platformConfig.local) {
212
- // If "both" mode and we successfully got cloud devices, enrich local config
213
- if (this.mode === 'both' && cloudDevices && this.deviceManager) {
214
- this.log.info('[Local] Enriching local config with cloud device details…');
215
- await this.enrichLocalConfigFromCloud(cloudDevices);
216
- }
217
- this.log.info('[Local] Initialising local device manager…');
218
- this.localDeviceManager = new LocalDeviceManager_1.default(this.platformConfig.local, this.log, this.api.user.storagePath());
219
- await this.localDeviceManager.initLocalDevices();
220
- this.localDeviceManager.connectAllDevices();
221
- this.localDeviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_ADD, (device) => this.addAccessory(device, 'local'));
222
- this.localDeviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this));
223
- this.localDeviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this));
224
- this.localDeviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_DELETE, this.removeAccessory.bind(this));
225
- const localDevices = [...this.localDeviceManager.devices.values()];
226
- this.log.info(`[Local] Registered ${localDevices.length} local device(s).`);
227
- for (const device of localDevices) {
228
- this.addAccessory(device, 'local');
229
- }
188
+ async saveDeviceList() {
189
+ if (!this.deviceManager) {
190
+ return;
230
191
  }
231
- // ── Cloud devices (if not already initialized in "both" mode) ──────────────
232
- if (cloudEnabled && this.options && this.mode !== 'both') {
233
- let devices;
234
- if (this.options.projectType === '1') {
235
- devices = await this.initCustomProject();
236
- }
237
- else if (this.options.projectType === '2') {
238
- devices = await this.initHomeProject();
239
- }
240
- else {
241
- this.log.warn(`Unsupported projectType: ${this.options['projectType']}.`);
242
- }
243
- if (devices && this.deviceManager) {
244
- // Initialize config hash tracker for cloud devices
245
- this.configHash = new ConfigHash_1.ConfigHash(this.api.user.storagePath(), 'tuya-cloud-configs');
246
- // Apply device config overrides
247
- for (const device of devices) {
248
- const deviceConfig = this.getDeviceConfig(device, 'cloud');
249
- if (deviceConfig?.category) {
250
- this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category);
251
- device.category = deviceConfig.category;
252
- }
253
- if (deviceConfig?.unbridged) {
254
- this.log.warn('Unbridge %o category %o', device.name, device.category);
255
- device.unbridged = deviceConfig.unbridged;
256
- }
257
- // Check if config has changed since last run
258
- // Hash the device override fields that affect accessory structure
259
- const configToHash = {
260
- deviceId: device.id,
261
- customCategory: deviceConfig?.category,
262
- unbridged: deviceConfig?.unbridged ?? false,
263
- schemaOverrides: deviceConfig?.schema ? JSON.stringify(deviceConfig.schema) : undefined,
264
- adaptiveLighting: deviceConfig?.adaptiveLighting ?? false,
265
- };
266
- const { changed: configChanged } = this.configHash.hasConfigChanged(device.id, configToHash);
267
- device.configChanged = configChanged;
268
- if (configChanged) {
269
- this.log.info(`[Cloud] Device config changed for "${device.name}" (${device.id}), will rebuild services`);
270
- }
271
- }
272
- await this.deviceManager.updateInfraredRemotes(devices);
273
- this.log.info(`[Cloud] Got ${devices.length} device(s) and scene(s).`);
274
- const file = path_1.default.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager.api.tokenInfo.uid}.json`);
275
- this.log.info('Device list saved at %s', file);
276
- if (!fs_1.default.existsSync(this.api.user.persistPath())) {
277
- await fs_1.default.promises.mkdir(this.api.user.persistPath());
278
- }
279
- await fs_1.default.promises.writeFile(file, JSON.stringify(devices, null, 2));
280
- for (const device of devices) {
281
- this.addAccessory(device, 'cloud');
282
- }
283
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_ADD, (device) => this.addAccessory(device, 'cloud'));
284
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this));
285
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this));
286
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_DELETE, this.removeAccessory.bind(this));
287
- }
192
+ if (this.tid !== undefined) {
193
+ clearTimeout(this.tid);
194
+ this.tid = undefined;
288
195
  }
289
- else if (cloudEnabled && this.mode === 'both' && cloudDevices && this.deviceManager) {
290
- // "both" mode: finish setting up cloud devices that were already initialized
291
- this.configHash = new ConfigHash_1.ConfigHash(this.api.user.storagePath(), 'tuya-cloud-configs');
292
- // Apply device config overrides
293
- for (const device of cloudDevices) {
294
- const deviceConfig = this.getDeviceConfig(device, 'cloud');
295
- if (deviceConfig?.category) {
296
- this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category);
297
- device.category = deviceConfig.category;
298
- }
299
- if (deviceConfig?.unbridged) {
300
- this.log.warn('Unbridge %o category %o', device.name, device.category);
301
- device.unbridged = deviceConfig.unbridged;
302
- }
303
- // Check if config has changed since last run
304
- const configToHash = {
305
- deviceId: device.id,
306
- customCategory: deviceConfig?.category,
307
- unbridged: deviceConfig?.unbridged ?? false,
308
- schemaOverrides: deviceConfig?.schema ? JSON.stringify(deviceConfig.schema) : undefined,
309
- adaptiveLighting: deviceConfig?.adaptiveLighting ?? false,
310
- };
311
- const { changed: configChanged } = this.configHash.hasConfigChanged(device.id, configToHash);
312
- device.configChanged = configChanged;
313
- if (configChanged) {
314
- this.log.info(`[Cloud] Device config changed for "${device.name}" (${device.id}), will rebuild services`);
315
- }
316
- }
317
- await this.deviceManager.updateInfraredRemotes(cloudDevices);
318
- this.log.info(`[Cloud] Got ${cloudDevices.length} device(s) and scene(s).`);
319
- const file = path_1.default.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager.api.tokenInfo.uid}.json`);
196
+ this.tid = setTimeout(async () => {
197
+ const file = path_1.default.join(this.api.user.persistPath(), `TuyaDeviceList.${this.platformID}-${this.mode}.json`);
320
198
  this.log.info('Device list saved at %s', file);
321
199
  if (!fs_1.default.existsSync(this.api.user.persistPath())) {
322
200
  await fs_1.default.promises.mkdir(this.api.user.persistPath());
323
201
  }
324
- await fs_1.default.promises.writeFile(file, JSON.stringify(cloudDevices, null, 2));
325
- for (const device of cloudDevices) {
326
- this.addAccessory(device, 'cloud');
327
- }
328
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_ADD, (device) => this.addAccessory(device, 'cloud'));
329
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this));
330
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this));
331
- if (this.localDeviceManager) {
332
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, (device, status) => {
333
- this.localDeviceManager?.handleCloudStatusUpdate(device.id, status);
334
- });
335
- }
336
- this.deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_DELETE, this.removeAccessory.bind(this));
337
- }
338
- // Remove stale cached accessories not claimed by any device
339
- for (const cachedAccessory of this.cachedAccessories) {
340
- this.log.warn('Removing unused accessory from cache:', cachedAccessory.displayName);
341
- this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [cachedAccessory]);
342
- }
343
- this.cachedAccessories = [];
344
- }
345
- getDeviceConfig(device, source) {
346
- if (!this.options.deviceOverrides) {
347
- return undefined;
348
- }
349
- // Find matching override, respecting source filtering
350
- // Since deviceOverrides are in the cloud config, they default to cloud-only
351
- const effectiveSource = source || 'cloud';
352
- const matches = this.options.deviceOverrides.filter(config => {
353
- const sourceMatch = config.source
354
- ? (config.source === 'both' || config.source === effectiveSource) // explicit source
355
- : effectiveSource === 'cloud'; // default: cloud-only
356
- const idMatch = config.id === device.id || config.id === device.uuid ||
357
- config.id === device.product_id || config.id === 'global';
358
- return sourceMatch && idMatch;
359
- });
360
- // Return device-specific config, then product, then global
361
- return matches.find(config => config.id === device.id || config.id === device.uuid) ||
362
- matches.find(config => config.id === device.product_id) ||
363
- matches.find(config => config.id === 'global');
364
- }
365
- getDeviceSchemaConfig(device, code, source) {
366
- const deviceConfig = this.getDeviceConfig(device, source);
367
- if (!deviceConfig || !deviceConfig.schema) {
368
- return undefined;
369
- }
370
- // migrate old config
371
- deviceConfig.schema.forEach(item => {
372
- if (item['oldCode']) {
373
- item.newCode = item.code;
374
- item.code = item['oldCode'];
375
- item['oldCode'] = undefined;
376
- }
377
- });
378
- // ignore case - allow both old (code) and migrated (newCode) names
379
- const schemaConfig = deviceConfig.schema.find(item => {
380
- if (!code) {
381
- return false;
382
- }
383
- const target = code.toString().toLowerCase();
384
- const legacyCode = item.code?.toString().toLowerCase();
385
- const migratedCode = item.newCode?.toString().toLowerCase();
386
- return legacyCode === target || migratedCode === target;
387
- });
388
- if (!schemaConfig) {
389
- return undefined;
390
- }
391
- return schemaConfig;
202
+ const devices = [...this.platformAccessories.values()].map(accessory => this.deviceManager.getDevice(accessory.context.deviceID));
203
+ await fs_1.default.promises.writeFile(file, JSON.stringify(devices, null, 2));
204
+ this.tid = undefined;
205
+ }, 5000);
392
206
  }
393
207
  /**
394
- * Enrich local device config with cloud device details (local_key, ip, etc.)
395
- * Called during "both" mode initialization to populate local credentials from cloud API
208
+ * This is an example method showing how to register discovered accessories.
209
+ * Accessories must only be registered once, previously created accessories
210
+ * must not be registered again to prevent "duplicate UUID" errors.
396
211
  */
397
- async enrichLocalConfigFromCloud(devices) {
398
- if (!this.deviceManager || !this.platformConfig.local || !this.platformConfig.local.devices) {
399
- return;
400
- }
401
- // Create a map of cloud devices by ID for quick lookup
402
- const cloudDeviceMap = new Map(devices.map(d => [d.id, d]));
403
- for (const device of devices) {
404
- try {
405
- // Skip if already manually configured in local section
406
- const existingConfig = this.platformConfig.local.devices.find(cfg => cfg.tuyaDeviceId === device.id || cfg.tuyaDeviceId === device.uuid);
407
- if (existingConfig && existingConfig.tuyaKey) {
408
- this.log.debug(`[Hybrid] Device ${device.name} (${device.id}) already has manual local config, skipping cloud enrichment`);
409
- continue;
410
- }
411
- // Fetch device details from cloud API (includes local_key)
412
- const detailRes = await this.deviceManager.getDeviceDetails(device.id);
413
- if (!detailRes.success || !detailRes.result) {
414
- this.log.debug(`[Hybrid] Could not fetch device details for ${device.name} (${device.id}) from cloud API`);
415
- continue;
416
- }
417
- const details = detailRes.result;
418
- const deviceId = device.id || device.uuid;
419
- const localKey = details.local_key || details.localKey;
420
- const ip = details.ip || details.address;
421
- if (!deviceId) {
422
- this.log.warn(`[Hybrid] Skipping enrichment for ${device.name} because cloud device ID is missing`);
423
- continue;
424
- }
425
- if (!localKey) {
426
- this.log.debug(`[Hybrid] No local_key available for ${device.name} (${deviceId}), will use cloud-only`);
427
- continue;
428
- }
429
- if (existingConfig) {
430
- // Update existing manual config with cloud-provided local_key and optional IP
431
- existingConfig.tuyaKey = localKey;
432
- if (ip) {
433
- existingConfig.ip = ip;
434
- }
435
- this.log.info(`[Hybrid] Enriched local config for ${device.name} (${deviceId}) with cloud-provided local_key and ip`);
436
- }
437
- else {
438
- // Create new local device config entry from cloud device details
439
- const localDeviceConfig = {
440
- tuyaDeviceId: deviceId,
441
- tuyaKey: localKey,
442
- name: device.name || `LocalDevice-${deviceId.slice(0, 8)}`,
443
- category: device.category,
444
- };
445
- if (ip) {
446
- localDeviceConfig.ip = ip;
447
- }
448
- if (device.product_id) {
449
- localDeviceConfig.productId = device.product_id;
450
- }
451
- if (!this.platformConfig.local.devices) {
452
- this.platformConfig.local.devices = [];
453
- }
454
- this.platformConfig.local.devices.push(localDeviceConfig);
455
- this.log.info(`[Hybrid] Added cloud device ${device.name} (${deviceId}) to local config with local_key`);
456
- }
457
- }
458
- catch (error) {
459
- const msg = error instanceof Error ? error.message : String(error);
460
- this.log.warn(`[Hybrid] Error enriching device ${device.name} (${device.id}): ${msg}`);
461
- // Continue with next device - this is non-fatal
462
- }
212
+ async initDevices() {
213
+ let deviceManager;
214
+ if (this.mode === config_1.TuyaPluginMode.cloud) {
215
+ deviceManager = await this.initCloudMode(this.platformConfig.cloud);
463
216
  }
464
- }
465
- async initCustomProject() {
466
- if (this.options.projectType !== '1') {
467
- return undefined;
468
- }
469
- const DEFAULT_USER = 'homebridge';
470
- const DEFAULT_PASS = 'homebridge';
471
- let res;
472
- const { endpoint, accessId, accessKey, debug, debugLevel } = this.options;
473
- const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true);
474
- const api = new TuyaOpenAPI_1.default(endpoint, accessId, accessKey, this.log, 'en', debugMode);
475
- const deviceManager = new TuyaCustomDeviceManager_1.default(api, debugMode);
476
- this.log.info('Get token.');
477
- res = await api.getToken();
478
- if (res.success === false) {
479
- this.log.error(`Get token failed. code=${res.code}, msg=${res.msg}`);
480
- return undefined;
481
- }
482
- this.log.info(`Search default user "${DEFAULT_USER}"`);
483
- res = await api.customGetUserInfo(DEFAULT_USER);
484
- if (res.success === false) {
485
- this.log.error(`Search user failed. code=${res.code}, msg=${res.msg}`);
486
- return undefined;
487
- }
488
- if (!res.result.user_name) {
489
- this.log.info(`Default user "${DEFAULT_USER}" not exist.`);
490
- this.log.info(`Creating default user "${DEFAULT_USER}".`);
491
- res = await api.customCreateUser(DEFAULT_USER, DEFAULT_PASS);
492
- if (res.success === false) {
493
- this.log.error(`Create default user failed. code=${res.code}, msg=${res.msg}`);
494
- return undefined;
495
- }
217
+ else if (this.mode === config_1.TuyaPluginMode.local) {
218
+ deviceManager = await this.initLocalMode(this.platformConfig.local);
496
219
  }
497
- else {
498
- this.log.info(`Default user "${DEFAULT_USER}" exists.`);
499
- }
500
- const uid = res.result.user_id;
501
- this.log.info('Fetching asset list.');
502
- res = await deviceManager.getAssetList();
503
- if (res.success === false) {
504
- this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`);
505
- return undefined;
506
- }
507
- const assetIDList = [];
508
- for (const { asset_id, asset_name } of res.result.list) {
509
- this.log.info(`Got asset_id=${asset_id}, asset_name=${asset_name}`);
510
- assetIDList.push(asset_id);
511
- }
512
- if (assetIDList.length === 0) {
513
- this.log.warn('Asset list is empty. exit.');
514
- return undefined;
515
- }
516
- this.log.info('Authorize asset list.');
517
- res = await deviceManager.authorizeAssetList(uid, assetIDList, true);
518
- if (res.success === false) {
519
- this.log.error(`Authorize asset list failed. code=${res.code}, msg=${res.msg}`);
520
- return undefined;
521
- }
522
- this.log.info(`Log in with user "${DEFAULT_USER}".`);
523
- res = await api.customLogin(DEFAULT_USER, DEFAULT_USER);
524
- if (res.success === false) {
525
- this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`);
526
- if (res.code && TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]) {
527
- this.log.error(TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]);
528
- }
529
- return undefined;
220
+ else if (this.mode === config_1.TuyaPluginMode.both) {
221
+ deviceManager = await this.initBothMode(this.platformConfig.cloud, this.platformConfig.local);
530
222
  }
531
- this.log.info('Start MQTT connection.');
532
- deviceManager.mq.start();
533
- this.log.info('Fetching device list.');
534
- deviceManager.ownerIDs = assetIDList;
535
- const devices = await deviceManager.updateDevices(assetIDList);
536
223
  this.deviceManager = deviceManager;
537
- return devices;
538
- }
539
- async initHomeProject() {
540
- if (this.options.projectType !== '2') {
541
- return undefined;
542
- }
543
- let res;
544
- const { accessId, accessKey, countryCode, username, password, appSchema, endpoint, debug, debugLevel } = this.options;
545
- const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true);
546
- const api = new TuyaOpenAPI_1.default((endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI_1.default.getDefaultEndpoint(countryCode), accessId, accessKey, this.log, 'en', debugMode);
547
- const deviceManager = new TuyaHomeDeviceManager_1.default(api, debugMode);
548
- this.log.info('Log in to Tuya Cloud.');
549
- res = await api.homeLogin(countryCode, username, password, appSchema);
550
- if (res.success === false) {
551
- this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`);
552
- if (res.code && TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]) {
553
- this.log.error(TuyaOpenAPI_1.LOGIN_ERROR_MESSAGES[res.code]);
554
- }
555
- return undefined;
556
- }
557
- this.log.info('Start MQTT connection.');
558
- deviceManager.mq.start();
559
- this.log.info('Fetching home list.');
560
- res = await deviceManager.getHomeList();
561
- if (res.success === false) {
562
- this.log.error(`Fetching home list failed. code=${res.code}, msg=${res.msg}`);
563
- return undefined;
564
- }
565
- const homeIDList = [];
566
- for (const { home_id, name } of res.result) {
567
- this.log.info(`Got home_id=${home_id}, name=${name}`);
568
- if (this.options.homeWhitelist) {
569
- if (this.options.homeWhitelist.includes(home_id)) {
570
- this.log.info(`Found home_id=${home_id} in whitelist; including devices from this home.`);
571
- homeIDList.push(home_id);
572
- }
573
- else {
574
- this.log.info(`Did not find home_id=${home_id} in whitelist; excluding devices from this home.`);
575
- }
576
- }
577
- else {
578
- homeIDList.push(home_id);
579
- }
580
- }
581
- if (homeIDList.length === 0) {
582
- this.log.warn('Home list is empty.');
583
- }
584
- this.log.info('Fetching device list.');
585
- deviceManager.ownerIDs = homeIDList.map(homeID => homeID.toString());
586
- const devices = await deviceManager.updateDevices(homeIDList);
587
- this.log.info('Fetching scene list.');
588
- for (const homeID of homeIDList) {
589
- const scenes = await deviceManager.getSceneList(homeID);
590
- for (const scene of scenes) {
591
- this.log.info(`Got scene_id=${scene.id}, name=${scene.name}`);
592
- }
593
- devices.push(...scenes);
224
+ const devices = await this.deviceManager.pullDevices();
225
+ devices.forEach(device => this.log.debug(`${device.id}: ${device.name}`));
226
+ for (const device of devices) {
227
+ this.addAccessory(device);
228
+ }
229
+ // add RTSP camera accessories that are not present in either Cloud or Local (non-Tuya devices).
230
+ this.platformConfig.local?.cameras?.
231
+ filter(cconfig => !devices.map(device => device.id).includes(cconfig.deviceId)).forEach(cconfig => {
232
+ this.log.info(`adding non-Tuya device: ${cconfig.deviceName}`);
233
+ const device = this.deviceManager.createRTSPCameraDevice(cconfig);
234
+ this.deviceManager.devices.push(device);
235
+ this.addAccessory(device);
236
+ });
237
+ deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_ADD, this.addAccessory.bind(this));
238
+ deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this));
239
+ deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this));
240
+ deviceManager.on(TuyaDeviceManager_1.default.Events.DEVICE_DELETE, this.removeAccessory.bind(this));
241
+ if (this.mode !== config_1.TuyaPluginMode.cloud) {
242
+ // When running in local mode, a short delay is added before detecting the change.
243
+ setTimeout(this.finishDeviceInitializing.bind(this), (this.platformConfig.local?.discoverTimeout ?? 5) * 1000);
594
244
  }
595
- this.deviceManager = deviceManager;
596
- if (this.options.generateWeatherAccessory) {
597
- const targetDevice = devices.find(device => device.lat && device.lon);
598
- if (targetDevice) {
599
- devices.push(this.createWeatherDevice(targetDevice, res.result));
600
- }
245
+ else {
246
+ this.finishDeviceInitializing();
601
247
  }
602
- return devices;
603
248
  }
604
- addAccessory(device, source) {
605
- // Apply device override config before checking if hidden
606
- const deviceConfig = this.getDeviceConfig(device, source);
607
- if (deviceConfig?.category) {
608
- this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category);
609
- device.category = deviceConfig.category;
249
+ async initCloudMode(cloudConfig) {
250
+ this.log.info('[Cloud] Initializing cloud device manager…');
251
+ let deviceManager;
252
+ if (cloudConfig.projectType === '1') {
253
+ deviceManager = await this.initCustomProject(cloudConfig);
610
254
  }
611
- if (deviceConfig?.unbridged) {
612
- this.log.warn('Unbridge %o category %o', device.name, device.category);
613
- device.unbridged = deviceConfig.unbridged;
255
+ else if (cloudConfig.projectType === '2') {
256
+ deviceManager = await this.initHomeProject(cloudConfig);
614
257
  }
258
+ return deviceManager;
259
+ }
260
+ async initLocalMode(localConfig) {
261
+ this.log.info('[Local] Initializing local device manager…');
262
+ return new LocalDeviceManager_1.default(localConfig, !!this.debug);
263
+ }
264
+ async initBothMode(cloudConfig, localConfig) {
265
+ this.log.debug('[Hybrid] Initializing...');
266
+ const { accessId, accessKey, endpoint, forceIPv4 } = cloudConfig;
267
+ const debugMode = !!this.debugLevel ? this.debugLevel.includes('api') : this.debug;
268
+ const _endpoint = (endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI_1.default.getDefaultEndpoint(cloudConfig['countryCode']);
269
+ const api = new TuyaOpenAPI_1.default(_endpoint, accessId, accessKey, 'en', debugMode, forceIPv4);
270
+ return new TuyaHybridDeviceManager_1.default(api, cloudConfig, localConfig, !!this.debug);
271
+ }
272
+ async initCustomProject(config) {
273
+ const { endpoint, accessId, accessKey, forceIPv4 } = config;
274
+ const debugMode = !!this.debugLevel ? this.debugLevel.includes('api') : this.debug;
275
+ const api = new TuyaOpenAPI_1.default(endpoint, accessId, accessKey, 'en', debugMode, forceIPv4);
276
+ return new TuyaCustomDeviceManager_1.default(api, config, debugMode);
277
+ }
278
+ async initHomeProject(config) {
279
+ const { accessId, accessKey, countryCode, endpoint, forceIPv4 } = config;
280
+ const debugMode = !!this.debugLevel ? this.debugLevel.includes('api') : this.debug;
281
+ const _endpoint = (endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI_1.default.getDefaultEndpoint(countryCode);
282
+ const api = new TuyaOpenAPI_1.default(_endpoint, accessId, accessKey, 'en', debugMode, forceIPv4);
283
+ return new TuyaHomeDeviceManager_1.default(api, config, debugMode);
284
+ }
285
+ addAccessory(device) {
615
286
  if (device.category === 'hidden') {
616
287
  this.log.info('Hide Accessory:', device.name);
617
- return;
288
+ return null;
618
289
  }
619
290
  const uuid = this.api.hap.uuid.generate(device.id);
620
- const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid);
291
+ const existingAccessory = this.platformAccessories.get(uuid);
621
292
  if (existingAccessory && !device.unbridged) {
622
- this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
623
- // Update context
624
- if (!existingAccessory.context || !existingAccessory.context.deviceID) {
625
- this.log.info('Update accessory context:', existingAccessory.displayName);
626
- existingAccessory.context.deviceID = device.id;
627
- this.api.updatePlatformAccessories([existingAccessory]);
628
- }
293
+ this.log.debug('Restoring existing accessory from cache:', existingAccessory.displayName);
629
294
  // create the accessory handler for the restored accessory
630
295
  const handler = AccessoryFactory_1.default.createAccessory(this, existingAccessory, device);
631
296
  this.accessoryHandlers.push(handler);
632
- const index = this.cachedAccessories.indexOf(existingAccessory);
633
- if (index >= 0) {
634
- this.cachedAccessories.splice(index, 1);
635
- }
297
+ // Always apply the user settings to ensure that any changes made after the cache was created are properly reflected.
298
+ AccessoryFactory_1.default.configAccessory(this, existingAccessory);
299
+ this.syncDeviceAndAccessory(device, existingAccessory);
300
+ this.saveDeviceList();
301
+ return existingAccessory;
636
302
  }
637
303
  else {
638
304
  // the accessory does not yet exist, so we need to create it
639
- this.log.info('Adding new accessory:', device.name);
305
+ this.log.debug('Adding new accessory:', device.name);
640
306
  // create a new accessory (sanitize name to conform to HAP rules)
641
307
  const safeName = (0, util_1.sanitizeName)(device.name) ?? (device.id || 'Tuya Device');
642
308
  const accessory = new this.api.platformAccessory(safeName, uuid);
643
309
  accessory.context.deviceID = device.id;
310
+ // Initialize config hash tracker for device
311
+ const currentHash = this.deviceManager.createDeviceConfigHash(device);
312
+ accessory.context.deviceConfigHash = currentHash;
644
313
  // create the accessory handler for the newly create accessory
645
314
  const handler = AccessoryFactory_1.default.createAccessory(this, accessory, device);
646
315
  this.accessoryHandlers.push(handler);
316
+ AccessoryFactory_1.default.configAccessory(this, accessory);
647
317
  // link the accessory to your platform
648
318
  if (device.unbridged) {
319
+ this.log.success(`(unbridged) publish accessory: ${accessory.displayName}`);
649
320
  this.api.publishExternalAccessories(settings_1.PLUGIN_NAME, [accessory]);
650
321
  }
651
322
  else {
323
+ this.log.success(`publish accessory: ${accessory.displayName}`);
652
324
  this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
653
325
  }
654
- AccessoryFactory_1.default.configAccessory(this, accessory);
326
+ this.platformAccessories.set(accessory.UUID, accessory);
327
+ this.saveDeviceList();
328
+ return accessory;
655
329
  }
656
330
  }
657
331
  updateAccessoryInfo(device, info) {
@@ -659,8 +333,11 @@ class TuyaPlatform {
659
333
  if (!handler) {
660
334
  return;
661
335
  }
662
- // this.log.debug('onDeviceInfoUpdate devId = %s, status = %o}', device.id, info);
663
- handler.onDeviceInfoUpdate(info);
336
+ const accessory = this.platformAccessories.get(this.api.hap.uuid.generate(device.id));
337
+ handler.onDeviceInfoUpdate(info).then(() => {
338
+ this.syncDeviceAndAccessory(device, accessory);
339
+ });
340
+ this.saveDeviceList();
664
341
  }
665
342
  updateAccessoryStatus(device, status) {
666
343
  const handler = this.getAccessoryHandler(device.id);
@@ -669,6 +346,7 @@ class TuyaPlatform {
669
346
  }
670
347
  // this.log.debug('onDeviceStatusUpdate devId = %s, status = %o}', device.id, status);
671
348
  handler.onDeviceStatusUpdate(status);
349
+ // Status updates occur frequently, so the saveDeviceList process is not performed.
672
350
  }
673
351
  removeAccessory(deviceID) {
674
352
  const handler = this.getAccessoryHandler(deviceID);
@@ -679,21 +357,103 @@ class TuyaPlatform {
679
357
  if (index >= 0) {
680
358
  this.accessoryHandlers.splice(index, 1);
681
359
  }
360
+ this.saveDeviceList();
682
361
  this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [handler.accessory]);
683
362
  this.log.info('Removing existing accessory from cache:', handler.accessory.displayName);
363
+ this.platformAccessories.delete(handler.accessory.UUID);
364
+ }
365
+ async finishDeviceInitializing() {
366
+ // Remove stale cached accessories not claimed by any device
367
+ for (const [uuid, accessory] of this.platformAccessories) {
368
+ if (this.deviceManager.getDevice(accessory.context.deviceID) === undefined) {
369
+ this.log.warn('Removing unused accessory from cache:', accessory.displayName);
370
+ this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
371
+ this.platformAccessories.delete(uuid);
372
+ }
373
+ }
684
374
  }
685
375
  getAccessoryHandler(deviceID) {
686
376
  return this.accessoryHandlers.find(handler => handler.device?.id === deviceID);
687
377
  }
688
- createWeatherDevice(device, result) {
689
- const key = `weather-${device.owner_id}`;
690
- const uuid = this.api.hap.uuid.generate(key);
691
- this.log.info(`add weather device:${key}`);
692
- const virtualDevice = this.deviceManager.createVirtualDevice(device, uuid);
693
- virtualDevice.product_id = 'virtual-product-id-weather';
694
- virtualDevice.category = 'wsdcg';
695
- virtualDevice.name = `Weather(${result.find(home => home.home_id === device.owner_id)?.name})`;
696
- return virtualDevice;
378
+ /**
379
+ * When device information is updated due to changes in the plugin settings,
380
+ * the updated data must be reflected in the accessory cache.
381
+ * @param device
382
+ * @param accessory
383
+ */
384
+ async syncDeviceAndAccessory(device, accessory) {
385
+ this.log.debug(`sync device to accessory cache. device: ${device.name}, accessory cache: ${accessory.displayName}`);
386
+ // A code value used to detect changes in the plugin settings.
387
+ const currentHash = this.deviceManager.createDeviceConfigHash(device);
388
+ if (currentHash !== accessory.context.deviceConfigHash) {
389
+ this.log.info(`Device config changed for "${device.name}" (${device.id}), will rebuild services`);
390
+ accessory.context.deviceConfigHash = currentHash;
391
+ // To reflect configuration changes without discarding the existing
392
+ // cache instance as much as possible, the latest configuration data is created as a separate accessory.
393
+ const cloneDevice = Object.assign(new TuyaDevice_1.default({}), (0, util_1.deepClone)(device));
394
+ this.deviceManager.devices.push(cloneDevice);
395
+ cloneDevice.id = 'dummyID' + cloneDevice.id;
396
+ cloneDevice.name = 'dummy';
397
+ let cloudConfig = this.platformConfig.cloud?.deviceOverrides?.find(i => i.id === device.id);
398
+ let localConfig = this.platformConfig.local?.devices?.find(i => i.tuyaDeviceId === device.id);
399
+ let serviceConfig = this.platformConfig.cloud?.serviceInformationOverrides?.find(i => i.device_id === device.id);
400
+ if (cloudConfig) {
401
+ cloudConfig = (0, util_1.deepClone)(cloudConfig);
402
+ cloudConfig.id = cloneDevice.id;
403
+ this.platformConfig.cloud.deviceOverrides.push(cloudConfig);
404
+ }
405
+ if (localConfig) {
406
+ localConfig = (0, util_1.deepClone)(localConfig);
407
+ localConfig.tuyaDeviceId = cloneDevice.id;
408
+ this.platformConfig.local.devices.push(localConfig);
409
+ }
410
+ if (serviceConfig) {
411
+ serviceConfig = (0, util_1.deepClone)(serviceConfig);
412
+ serviceConfig.device_id = cloneDevice.id;
413
+ this.platformConfig.cloud.serviceInformationOverrides.push(serviceConfig);
414
+ }
415
+ const freshAccessory = this.addAccessory(cloneDevice);
416
+ // The dummy item should be removed immediately.
417
+ this.removeAccessory(cloneDevice.id);
418
+ this.deviceManager.devices.splice(this.deviceManager.devices.indexOf(cloneDevice), 1);
419
+ if (cloudConfig) {
420
+ this.platformConfig.cloud.deviceOverrides.splice(this.platformConfig.cloud.deviceOverrides.indexOf(cloudConfig), 1);
421
+ }
422
+ if (localConfig) {
423
+ localConfig.tuyaDeviceId = cloneDevice.id;
424
+ this.platformConfig.local.devices.splice(this.platformConfig.local.devices.indexOf(localConfig), 1);
425
+ }
426
+ if (serviceConfig) {
427
+ serviceConfig.device_id = cloudConfig.id;
428
+ this.platformConfig.cloud.serviceInformationOverrides.splice(this.platformConfig.cloud.serviceInformationOverrides.indexOf(serviceConfig), 1);
429
+ }
430
+ if (freshAccessory === null) {
431
+ // hidden
432
+ this.removeAccessory(device.id);
433
+ return;
434
+ }
435
+ const newServices = freshAccessory.services;
436
+ const oldServices = accessory.services;
437
+ for (const oldService of oldServices) {
438
+ const existsInNew = newServices.some(newService => newService.UUID === oldService.UUID);
439
+ if (!existsInNew) {
440
+ accessory.removeService(oldService);
441
+ continue;
442
+ }
443
+ const newService = newServices.find(s => s.UUID === oldService.UUID);
444
+ if (!newService) {
445
+ continue;
446
+ }
447
+ for (const oldChar of oldService.characteristics) {
448
+ const existsCharInNew = newService.characteristics.some(newChar => newChar.UUID === oldChar.UUID);
449
+ if (!existsCharInNew) {
450
+ oldService.removeCharacteristic(oldChar);
451
+ }
452
+ }
453
+ }
454
+ }
455
+ this.log.success(`(update) publish accessory: ${accessory.displayName}`);
456
+ this.api.updatePlatformAccessories([accessory]);
697
457
  }
698
458
  }
699
459
  exports.TuyaPlatform = TuyaPlatform;