@homebridge-plugins/homebridge-tuya 3.0.0-beta.7 → 3.0.0-beta.8

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