@homebridge-plugins/homebridge-tuya 2.2.1 → 3.0.0-beta.1

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 (341) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +161 -144
  3. package/config.schema.json +976 -11
  4. package/dist/{core → cloud/api}/TuyaOpenAPI.js +6 -7
  5. package/dist/cloud/api/TuyaOpenAPI.js.map +1 -0
  6. package/dist/{core → cloud/api}/TuyaOpenMQ.js +13 -11
  7. package/dist/cloud/api/TuyaOpenMQ.js.map +1 -0
  8. package/dist/cloud/device/TuyaCustomDeviceManager.js.map +1 -0
  9. package/dist/{device → cloud/device}/TuyaDevice.js +14 -1
  10. package/dist/cloud/device/TuyaDevice.js.map +1 -0
  11. package/dist/{device → cloud/device}/TuyaDeviceManager.js +8 -3
  12. package/dist/cloud/device/TuyaDeviceManager.js.map +1 -0
  13. package/dist/cloud/device/TuyaHomeDeviceManager.js.map +1 -0
  14. package/dist/config.js +1 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/local/CloudLocalReconciliation.js +180 -0
  17. package/dist/local/CloudLocalReconciliation.js.map +1 -0
  18. package/dist/local/DynamicChildDiscovery.js +154 -0
  19. package/dist/local/DynamicChildDiscovery.js.map +1 -0
  20. package/dist/local/LocalDevice.js +419 -0
  21. package/dist/local/LocalDevice.js.map +1 -0
  22. package/dist/local/LocalDeviceManager.js +833 -0
  23. package/dist/local/LocalDeviceManager.js.map +1 -0
  24. package/dist/local/TuyaDiscovery.js +248 -0
  25. package/dist/local/TuyaDiscovery.js.map +1 -0
  26. package/dist/local/ZigbeeEventDetection.js +213 -0
  27. package/dist/local/ZigbeeEventDetection.js.map +1 -0
  28. package/dist/local/ZigbeeGatewayDetection.js +109 -0
  29. package/dist/local/ZigbeeGatewayDetection.js.map +1 -0
  30. package/dist/local/config.js +6 -0
  31. package/dist/local/config.js.map +1 -0
  32. package/dist/local/protocol/ChildPayloadUtility.js +94 -0
  33. package/dist/local/protocol/ChildPayloadUtility.js.map +1 -0
  34. package/dist/local/protocol/Protocol.js +3 -0
  35. package/dist/local/protocol/Protocol.js.map +1 -0
  36. package/dist/local/protocol/ProtocolFactory.js +27 -0
  37. package/dist/local/protocol/ProtocolFactory.js.map +1 -0
  38. package/dist/local/protocol/ProtocolUtilities.js +329 -0
  39. package/dist/local/protocol/ProtocolUtilities.js.map +1 -0
  40. package/dist/local/protocol/ProtocolV31V32.js +60 -0
  41. package/dist/local/protocol/ProtocolV31V32.js.map +1 -0
  42. package/dist/local/protocol/ProtocolV33.js +52 -0
  43. package/dist/local/protocol/ProtocolV33.js.map +1 -0
  44. package/dist/local/protocol/ProtocolV34.js +104 -0
  45. package/dist/local/protocol/ProtocolV34.js.map +1 -0
  46. package/dist/local/protocol/ProtocolV35.js +77 -0
  47. package/dist/local/protocol/ProtocolV35.js.map +1 -0
  48. package/dist/platform.js +149 -69
  49. package/dist/platform.js.map +1 -1
  50. package/dist/shared/AccessoryFactory.js +276 -0
  51. package/dist/shared/AccessoryFactory.js.map +1 -0
  52. package/dist/{accessory → shared/accessories}/AccessoryFactory.js +2 -2
  53. package/dist/shared/accessories/AccessoryFactory.js.map +1 -0
  54. package/dist/shared/accessories/AirConditionerAccessory.js.map +1 -0
  55. package/dist/{accessory → shared/accessories}/AirPurifierAccessory.js +1 -1
  56. package/dist/shared/accessories/AirPurifierAccessory.js.map +1 -0
  57. package/dist/shared/accessories/AirQualitySensorAccessory.js.map +1 -0
  58. package/dist/{accessory → shared/accessories}/BaseAccessory.js +99 -30
  59. package/dist/shared/accessories/BaseAccessory.js.map +1 -0
  60. package/dist/shared/accessories/BlindsAccessory.js +199 -0
  61. package/dist/shared/accessories/BlindsAccessory.js.map +1 -0
  62. package/dist/{accessory → shared/accessories}/CameraAccessory.js +7 -3
  63. package/dist/shared/accessories/CameraAccessory.js.map +1 -0
  64. package/dist/shared/accessories/CarbonDioxideSensorAccessory.js.map +1 -0
  65. package/dist/shared/accessories/CarbonMonoxideSensorAccessory.js.map +1 -0
  66. package/dist/shared/accessories/ContactSensorAccessory.js.map +1 -0
  67. package/dist/shared/accessories/DehumidifierAccessory.js.map +1 -0
  68. package/dist/shared/accessories/DiffuserAccessory.js.map +1 -0
  69. package/dist/shared/accessories/DimmerAccessory.js.map +1 -0
  70. package/dist/{accessory → shared/accessories}/DoorbellAccessory.js +3 -1
  71. package/dist/shared/accessories/DoorbellAccessory.js.map +1 -0
  72. package/dist/{accessory → shared/accessories}/ExtractionHoodAccessory.js +4 -2
  73. package/dist/shared/accessories/ExtractionHoodAccessory.js.map +1 -0
  74. package/dist/{accessory → shared/accessories}/FanAccessory.js +10 -4
  75. package/dist/shared/accessories/FanAccessory.js.map +1 -0
  76. package/dist/shared/accessories/GarageDoorAccessory.js.map +1 -0
  77. package/dist/shared/accessories/HeaterAccessory.js.map +1 -0
  78. package/dist/shared/accessories/HeaterAccessory_old.js.map +1 -0
  79. package/dist/shared/accessories/HumanPresenceSensorAccessory.js.map +1 -0
  80. package/dist/{accessory → shared/accessories}/HumidifierAccessory.js +3 -1
  81. package/dist/shared/accessories/HumidifierAccessory.js.map +1 -0
  82. package/dist/{accessory → shared/accessories}/IRAirConditionerAccessory.js +2 -2
  83. package/dist/shared/accessories/IRAirConditionerAccessory.js.map +1 -0
  84. package/dist/shared/accessories/IRControlHubAccessory.js.map +1 -0
  85. package/dist/shared/accessories/IRControlHubSubAccessory.js.map +1 -0
  86. package/dist/shared/accessories/IRGenericAccessory.js.map +1 -0
  87. package/dist/shared/accessories/LeakSensorAccessory.js.map +1 -0
  88. package/dist/shared/accessories/LightAccessory.js.map +1 -0
  89. package/dist/shared/accessories/LightSensorAccessory.js.map +1 -0
  90. package/dist/shared/accessories/LocationWeatherAccessory.js.map +1 -0
  91. package/dist/shared/accessories/LockAccessory.js.map +1 -0
  92. package/dist/shared/accessories/MotionSensorAccessory.js.map +1 -0
  93. package/dist/shared/accessories/OutletAccessory.js.map +1 -0
  94. package/dist/shared/accessories/PetFeederAccessory.js.map +1 -0
  95. package/dist/shared/accessories/SceneAccessory.js.map +1 -0
  96. package/dist/{accessory → shared/accessories}/SceneSwitchAccessory.js +1 -1
  97. package/dist/shared/accessories/SceneSwitchAccessory.js.map +1 -0
  98. package/dist/shared/accessories/SecuritySystemAccessory.js.map +1 -0
  99. package/dist/shared/accessories/SmokeSensorAccessory.js.map +1 -0
  100. package/dist/shared/accessories/SwitchAccessory.js +148 -0
  101. package/dist/shared/accessories/SwitchAccessory.js.map +1 -0
  102. package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js.map +1 -0
  103. package/dist/shared/accessories/ThermostatAccessory.js.map +1 -0
  104. package/dist/shared/accessories/TowerRackAccessory.js.map +1 -0
  105. package/dist/{accessory → shared/accessories}/ValveAccessory.js +1 -1
  106. package/dist/shared/accessories/ValveAccessory.js.map +1 -0
  107. package/dist/{accessory → shared/accessories}/VibrationSensorAccessory.js +7 -3
  108. package/dist/shared/accessories/VibrationSensorAccessory.js.map +1 -0
  109. package/dist/shared/accessories/WeatherStationAccessory.js.map +1 -0
  110. package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js.map +1 -0
  111. package/dist/shared/accessories/WhiteNoiseLightAccessory.js.map +1 -0
  112. package/dist/shared/accessories/WindowAccessory.js.map +1 -0
  113. package/dist/shared/accessories/WindowCoveringAccessory.js.map +1 -0
  114. package/dist/shared/accessories/WirelessSwitchAccessory.js.map +1 -0
  115. package/dist/shared/accessories/characteristic/Active.js.map +1 -0
  116. package/dist/{accessory → shared/accessories}/characteristic/AirQuality.js +1 -1
  117. package/dist/shared/accessories/characteristic/AirQuality.js.map +1 -0
  118. package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js.map +1 -0
  119. package/dist/shared/accessories/characteristic/CurrentTemperature.js.map +1 -0
  120. package/dist/shared/accessories/characteristic/CurrentWeather.js.map +1 -0
  121. package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js.map +1 -0
  122. package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js.map +1 -0
  123. package/dist/shared/accessories/characteristic/EnergyUsage.js.map +1 -0
  124. package/dist/shared/accessories/characteristic/Light.js.map +1 -0
  125. package/dist/shared/accessories/characteristic/LightSensor.js.map +1 -0
  126. package/dist/shared/accessories/characteristic/LockPhysicalControls.js.map +1 -0
  127. package/dist/{accessory → shared/accessories}/characteristic/MotionDetected.js +1 -1
  128. package/dist/shared/accessories/characteristic/MotionDetected.js.map +1 -0
  129. package/dist/shared/accessories/characteristic/Name.js.map +1 -0
  130. package/dist/shared/accessories/characteristic/OccupancyDetected.js.map +1 -0
  131. package/dist/{accessory → shared/accessories}/characteristic/On.js +1 -1
  132. package/dist/shared/accessories/characteristic/On.js.map +1 -0
  133. package/dist/shared/accessories/characteristic/OutletInUse.js.map +1 -0
  134. package/dist/{accessory → shared/accessories}/characteristic/ProgrammableSwitchEvent.js +2 -2
  135. package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js.map +1 -0
  136. package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js.map +1 -0
  137. package/dist/shared/accessories/characteristic/RotationSpeed.js.map +1 -0
  138. package/dist/shared/accessories/characteristic/SecuritySystemState.js.map +1 -0
  139. package/dist/shared/accessories/characteristic/SwingMode.js.map +1 -0
  140. package/dist/shared/accessories/characteristic/TargetTemperature.js.map +1 -0
  141. package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js.map +1 -0
  142. package/dist/shared/util/ConfigHash.js +79 -0
  143. package/dist/shared/util/ConfigHash.js.map +1 -0
  144. package/dist/shared/util/FfmpegStreamingProcess.js.map +1 -0
  145. package/dist/shared/util/InfraredTool.js.map +1 -0
  146. package/dist/shared/util/Logger.js.map +1 -0
  147. package/dist/shared/util/TuyaRecordingDelegate.js.map +1 -0
  148. package/dist/shared/util/TuyaStreamDelegate.js.map +1 -0
  149. package/dist/shared/util/color.js.map +1 -0
  150. package/dist/shared/util/util.js +135 -0
  151. package/dist/shared/util/util.js.map +1 -0
  152. package/eslint.config.mjs +52 -0
  153. package/jest.config.js +6 -0
  154. package/package.json +22 -27
  155. package/test/__mocks__/@homebridge-camera-utils.js +10 -0
  156. package/test/__mocks__/color-convert.js +24 -0
  157. package/test/__mocks__/kelvin-to-rgb.js +21 -0
  158. package/test/accessoryFactory.test.ts +522 -0
  159. package/test/config.test.ts +430 -0
  160. package/test/custom.test.ts +4 -4
  161. package/test/errorHandling.test.ts +570 -0
  162. package/test/errorHandlingCoverage.test.ts +410 -0
  163. package/test/home.test.ts +4 -4
  164. package/test/localDevice.test.ts +616 -0
  165. package/test/localDeviceManager.behavior.test.ts +185 -0
  166. package/test/localDeviceManager.test.ts +268 -0
  167. package/test/mqttMessages.test.ts +495 -0
  168. package/test/platform.test.ts +515 -0
  169. package/test/platform.validation.local.test.ts +250 -0
  170. package/test/protocol.test.ts +446 -0
  171. package/test/protocolUtilities.test.ts +351 -0
  172. package/test/sanitizeName.test.ts +2 -1
  173. package/test/tuyaCustomDeviceManager.test.ts +469 -0
  174. package/test/tuyaDevice.test.ts +425 -0
  175. package/test/tuyaDeviceManager.test.ts +813 -0
  176. package/test/tuyaDiscovery.test.ts +439 -0
  177. package/test/tuyaHomeDeviceManager.test.ts +540 -0
  178. package/test/tuyaOpenAPI.test.ts +404 -0
  179. package/test/tuyaOpenMQ.test.ts +444 -0
  180. package/test/util.test.ts +318 -0
  181. package/test/util.ts +4 -3
  182. package/test/zigbee/ChildPayloadUtility.test.ts +199 -0
  183. package/test/zigbee/CloudLocalReconciliation.test.ts +284 -0
  184. package/test/zigbee/DynamicChildDiscovery.test.ts +333 -0
  185. package/test/zigbee/LocalDevice.zigbee.test.ts +274 -0
  186. package/test/zigbee/LocalDeviceManager.cloud-discovery.test.ts +163 -0
  187. package/test/zigbee/LocalDeviceManager.zigbee.reconnect.test.ts +200 -0
  188. package/test/zigbee/LocalDeviceManager.zigbee.test.ts +344 -0
  189. package/test/zigbee/ZigbeeEventDetection.test.ts +418 -0
  190. package/test/zigbee/ZigbeeGatewayDetection.test.ts +222 -0
  191. package/tuya-local-configs/dev-1.hash +1 -0
  192. package/tuya-local-configs/device1.hash +1 -0
  193. package/wiki/Get-Local-Keys-for-Your-Devices.md +85 -0
  194. package/dist/accessory/AccessoryFactory.js.map +0 -1
  195. package/dist/accessory/AirConditionerAccessory.js.map +0 -1
  196. package/dist/accessory/AirPurifierAccessory.js.map +0 -1
  197. package/dist/accessory/AirQualitySensorAccessory.js.map +0 -1
  198. package/dist/accessory/BaseAccessory.js.map +0 -1
  199. package/dist/accessory/CameraAccessory.js.map +0 -1
  200. package/dist/accessory/CarbonDioxideSensorAccessory.js.map +0 -1
  201. package/dist/accessory/CarbonMonoxideSensorAccessory.js.map +0 -1
  202. package/dist/accessory/ContactSensorAccessory.js.map +0 -1
  203. package/dist/accessory/DehumidifierAccessory.js.map +0 -1
  204. package/dist/accessory/DiffuserAccessory.js.map +0 -1
  205. package/dist/accessory/DimmerAccessory.js.map +0 -1
  206. package/dist/accessory/DoorbellAccessory.js.map +0 -1
  207. package/dist/accessory/ExtractionHoodAccessory.js.map +0 -1
  208. package/dist/accessory/FanAccessory.js.map +0 -1
  209. package/dist/accessory/GarageDoorAccessory.js.map +0 -1
  210. package/dist/accessory/HeaterAccessory.js.map +0 -1
  211. package/dist/accessory/HeaterAccessory_old.js.map +0 -1
  212. package/dist/accessory/HumanPresenceSensorAccessory.js.map +0 -1
  213. package/dist/accessory/HumidifierAccessory.js.map +0 -1
  214. package/dist/accessory/IRAirConditionerAccessory.js.map +0 -1
  215. package/dist/accessory/IRControlHubAccessory.js.map +0 -1
  216. package/dist/accessory/IRControlHubSubAccessory.js.map +0 -1
  217. package/dist/accessory/IRGenericAccessory.js.map +0 -1
  218. package/dist/accessory/LeakSensorAccessory.js.map +0 -1
  219. package/dist/accessory/LightAccessory.js.map +0 -1
  220. package/dist/accessory/LightSensorAccessory.js.map +0 -1
  221. package/dist/accessory/LocationWeatherAccessory.js.map +0 -1
  222. package/dist/accessory/LockAccessory.js.map +0 -1
  223. package/dist/accessory/MotionSensorAccessory.js.map +0 -1
  224. package/dist/accessory/OutletAccessory.js.map +0 -1
  225. package/dist/accessory/PetFeederAccessory.js.map +0 -1
  226. package/dist/accessory/SceneAccessory.js.map +0 -1
  227. package/dist/accessory/SceneSwitchAccessory.js.map +0 -1
  228. package/dist/accessory/SecuritySystemAccessory.js.map +0 -1
  229. package/dist/accessory/SmokeSensorAccessory.js.map +0 -1
  230. package/dist/accessory/SwitchAccessory.js +0 -85
  231. package/dist/accessory/SwitchAccessory.js.map +0 -1
  232. package/dist/accessory/TemperatureHumiditySensorAccessory.js.map +0 -1
  233. package/dist/accessory/ThermostatAccessory.js.map +0 -1
  234. package/dist/accessory/TowerRackAccessory.js.map +0 -1
  235. package/dist/accessory/ValveAccessory.js.map +0 -1
  236. package/dist/accessory/VibrationSensorAccessory.js.map +0 -1
  237. package/dist/accessory/WeatherStationAccessory.js.map +0 -1
  238. package/dist/accessory/WetBulbGlobeTemperatureAccessory.js.map +0 -1
  239. package/dist/accessory/WhiteNoiseLightAccessory.js.map +0 -1
  240. package/dist/accessory/WindowAccessory.js.map +0 -1
  241. package/dist/accessory/WindowCoveringAccessory.js.map +0 -1
  242. package/dist/accessory/WirelessSwitchAccessory.js.map +0 -1
  243. package/dist/accessory/characteristic/Active.js.map +0 -1
  244. package/dist/accessory/characteristic/AirQuality.js.map +0 -1
  245. package/dist/accessory/characteristic/CurrentRelativeHumidity.js.map +0 -1
  246. package/dist/accessory/characteristic/CurrentTemperature.js.map +0 -1
  247. package/dist/accessory/characteristic/CurrentWeather.js.map +0 -1
  248. package/dist/accessory/characteristic/CurrentWeatherByOpenMeteo.js.map +0 -1
  249. package/dist/accessory/characteristic/CurrentWetBulbGlobeTemperature.js.map +0 -1
  250. package/dist/accessory/characteristic/EnergyUsage.js.map +0 -1
  251. package/dist/accessory/characteristic/Light.js.map +0 -1
  252. package/dist/accessory/characteristic/LightSensor.js.map +0 -1
  253. package/dist/accessory/characteristic/LockPhysicalControls.js.map +0 -1
  254. package/dist/accessory/characteristic/MotionDetected.js.map +0 -1
  255. package/dist/accessory/characteristic/Name.js.map +0 -1
  256. package/dist/accessory/characteristic/OccupancyDetected.js.map +0 -1
  257. package/dist/accessory/characteristic/On.js.map +0 -1
  258. package/dist/accessory/characteristic/OutletInUse.js.map +0 -1
  259. package/dist/accessory/characteristic/ProgrammableSwitchEvent.js.map +0 -1
  260. package/dist/accessory/characteristic/RelativeHumidityDehumidifierThreshold.js.map +0 -1
  261. package/dist/accessory/characteristic/RotationSpeed.js.map +0 -1
  262. package/dist/accessory/characteristic/SecuritySystemState.js.map +0 -1
  263. package/dist/accessory/characteristic/SwingMode.js.map +0 -1
  264. package/dist/accessory/characteristic/TargetTemperature.js.map +0 -1
  265. package/dist/accessory/characteristic/TemperatureDisplayUnits.js.map +0 -1
  266. package/dist/core/TuyaOpenAPI.js.map +0 -1
  267. package/dist/core/TuyaOpenMQ.js.map +0 -1
  268. package/dist/device/TuyaCustomDeviceManager.js.map +0 -1
  269. package/dist/device/TuyaDevice.js.map +0 -1
  270. package/dist/device/TuyaDeviceManager.js.map +0 -1
  271. package/dist/device/TuyaHomeDeviceManager.js.map +0 -1
  272. package/dist/util/FfmpegStreamingProcess.js.map +0 -1
  273. package/dist/util/InfraredTool.js.map +0 -1
  274. package/dist/util/Logger.js.map +0 -1
  275. package/dist/util/TuyaRecordingDelegate.js.map +0 -1
  276. package/dist/util/TuyaStreamDelegate.js.map +0 -1
  277. package/dist/util/color.js.map +0 -1
  278. package/dist/util/util.js +0 -63
  279. package/dist/util/util.js.map +0 -1
  280. /package/dist/{device → cloud/device}/TuyaCustomDeviceManager.js +0 -0
  281. /package/dist/{device → cloud/device}/TuyaHomeDeviceManager.js +0 -0
  282. /package/dist/{accessory → shared/accessories}/AirConditionerAccessory.js +0 -0
  283. /package/dist/{accessory → shared/accessories}/AirQualitySensorAccessory.js +0 -0
  284. /package/dist/{accessory → shared/accessories}/CarbonDioxideSensorAccessory.js +0 -0
  285. /package/dist/{accessory → shared/accessories}/CarbonMonoxideSensorAccessory.js +0 -0
  286. /package/dist/{accessory → shared/accessories}/ContactSensorAccessory.js +0 -0
  287. /package/dist/{accessory → shared/accessories}/DehumidifierAccessory.js +0 -0
  288. /package/dist/{accessory → shared/accessories}/DiffuserAccessory.js +0 -0
  289. /package/dist/{accessory → shared/accessories}/DimmerAccessory.js +0 -0
  290. /package/dist/{accessory → shared/accessories}/GarageDoorAccessory.js +0 -0
  291. /package/dist/{accessory → shared/accessories}/HeaterAccessory.js +0 -0
  292. /package/dist/{accessory → shared/accessories}/HeaterAccessory_old.js +0 -0
  293. /package/dist/{accessory → shared/accessories}/HumanPresenceSensorAccessory.js +0 -0
  294. /package/dist/{accessory → shared/accessories}/IRControlHubAccessory.js +0 -0
  295. /package/dist/{accessory → shared/accessories}/IRControlHubSubAccessory.js +0 -0
  296. /package/dist/{accessory → shared/accessories}/IRGenericAccessory.js +0 -0
  297. /package/dist/{accessory → shared/accessories}/LeakSensorAccessory.js +0 -0
  298. /package/dist/{accessory → shared/accessories}/LightAccessory.js +0 -0
  299. /package/dist/{accessory → shared/accessories}/LightSensorAccessory.js +0 -0
  300. /package/dist/{accessory → shared/accessories}/LocationWeatherAccessory.js +0 -0
  301. /package/dist/{accessory → shared/accessories}/LockAccessory.js +0 -0
  302. /package/dist/{accessory → shared/accessories}/MotionSensorAccessory.js +0 -0
  303. /package/dist/{accessory → shared/accessories}/OutletAccessory.js +0 -0
  304. /package/dist/{accessory → shared/accessories}/PetFeederAccessory.js +0 -0
  305. /package/dist/{accessory → shared/accessories}/SceneAccessory.js +0 -0
  306. /package/dist/{accessory → shared/accessories}/SecuritySystemAccessory.js +0 -0
  307. /package/dist/{accessory → shared/accessories}/SmokeSensorAccessory.js +0 -0
  308. /package/dist/{accessory → shared/accessories}/TemperatureHumiditySensorAccessory.js +0 -0
  309. /package/dist/{accessory → shared/accessories}/ThermostatAccessory.js +0 -0
  310. /package/dist/{accessory → shared/accessories}/TowerRackAccessory.js +0 -0
  311. /package/dist/{accessory → shared/accessories}/WeatherStationAccessory.js +0 -0
  312. /package/dist/{accessory → shared/accessories}/WetBulbGlobeTemperatureAccessory.js +0 -0
  313. /package/dist/{accessory → shared/accessories}/WhiteNoiseLightAccessory.js +0 -0
  314. /package/dist/{accessory → shared/accessories}/WindowAccessory.js +0 -0
  315. /package/dist/{accessory → shared/accessories}/WindowCoveringAccessory.js +0 -0
  316. /package/dist/{accessory → shared/accessories}/WirelessSwitchAccessory.js +0 -0
  317. /package/dist/{accessory → shared/accessories}/characteristic/Active.js +0 -0
  318. /package/dist/{accessory → shared/accessories}/characteristic/CurrentRelativeHumidity.js +0 -0
  319. /package/dist/{accessory → shared/accessories}/characteristic/CurrentTemperature.js +0 -0
  320. /package/dist/{accessory → shared/accessories}/characteristic/CurrentWeather.js +0 -0
  321. /package/dist/{accessory → shared/accessories}/characteristic/CurrentWeatherByOpenMeteo.js +0 -0
  322. /package/dist/{accessory → shared/accessories}/characteristic/CurrentWetBulbGlobeTemperature.js +0 -0
  323. /package/dist/{accessory → shared/accessories}/characteristic/EnergyUsage.js +0 -0
  324. /package/dist/{accessory → shared/accessories}/characteristic/Light.js +0 -0
  325. /package/dist/{accessory → shared/accessories}/characteristic/LightSensor.js +0 -0
  326. /package/dist/{accessory → shared/accessories}/characteristic/LockPhysicalControls.js +0 -0
  327. /package/dist/{accessory → shared/accessories}/characteristic/Name.js +0 -0
  328. /package/dist/{accessory → shared/accessories}/characteristic/OccupancyDetected.js +0 -0
  329. /package/dist/{accessory → shared/accessories}/characteristic/OutletInUse.js +0 -0
  330. /package/dist/{accessory → shared/accessories}/characteristic/RelativeHumidityDehumidifierThreshold.js +0 -0
  331. /package/dist/{accessory → shared/accessories}/characteristic/RotationSpeed.js +0 -0
  332. /package/dist/{accessory → shared/accessories}/characteristic/SecuritySystemState.js +0 -0
  333. /package/dist/{accessory → shared/accessories}/characteristic/SwingMode.js +0 -0
  334. /package/dist/{accessory → shared/accessories}/characteristic/TargetTemperature.js +0 -0
  335. /package/dist/{accessory → shared/accessories}/characteristic/TemperatureDisplayUnits.js +0 -0
  336. /package/dist/{util → shared/util}/FfmpegStreamingProcess.js +0 -0
  337. /package/dist/{util → shared/util}/InfraredTool.js +0 -0
  338. /package/dist/{util → shared/util}/Logger.js +0 -0
  339. /package/dist/{util → shared/util}/TuyaRecordingDelegate.js +0 -0
  340. /package/dist/{util → shared/util}/TuyaStreamDelegate.js +0 -0
  341. /package/dist/{util → shared/util}/color.js +0 -0
@@ -0,0 +1,250 @@
1
+ import { beforeEach, describe, expect, jest, test } from '@jest/globals';
2
+ import { TuyaPlatform } from '../src/platform';
3
+
4
+ jest.mock('../src/shared/util/Logger', () => ({
5
+ __esModule: true,
6
+ default: class Logger {
7
+ log() {}
8
+ info() {}
9
+ warn() {}
10
+ error() {}
11
+ },
12
+ PrefixLogger: class PrefixLogger {
13
+ constructor(public log: unknown, public name: string, public debug: boolean) {}
14
+ },
15
+ }));
16
+
17
+ jest.mock('../src/cloud/api/TuyaOpenAPI', () => {
18
+ return class MockTuyaOpenAPI {
19
+ log = console;
20
+ };
21
+ });
22
+
23
+ jest.mock('../src/cloud/device/TuyaDeviceManager', () => {
24
+ class MockTuyaDeviceManager {
25
+ static readonly Events = {
26
+ DEVICE_ADD: 'DEVICE_ADD',
27
+ DEVICE_INFO_UPDATE: 'DEVICE_INFO_UPDATE',
28
+ DEVICE_STATUS_UPDATE: 'DEVICE_STATUS_UPDATE',
29
+ DEVICE_DELETE: 'DEVICE_DELETE',
30
+ };
31
+ }
32
+ return MockTuyaDeviceManager;
33
+ });
34
+
35
+ jest.mock('../src/cloud/device/TuyaCustomDeviceManager', () => {
36
+ return class MockTuyaCustomDeviceManager {};
37
+ });
38
+
39
+ jest.mock('../src/cloud/device/TuyaHomeDeviceManager', () => {
40
+ return class MockTuyaHomeDeviceManager {};
41
+ });
42
+
43
+ jest.mock('../src/local/LocalDeviceManager', () => {
44
+ return class MockLocalDeviceManager {};
45
+ });
46
+
47
+ jest.mock('../src/shared/accessories/AccessoryFactory', () => ({
48
+ __esModule: true,
49
+ default: {
50
+ configAccessory: jest.fn(),
51
+ createAccessoryHandler: jest.fn(),
52
+ },
53
+ }));
54
+
55
+ jest.mock('../src/shared/util/util', () => ({
56
+ sanitizeName: (name: string) => name,
57
+ retry: jest.fn(async (fn: () => Promise<unknown>) => fn()),
58
+ }));
59
+
60
+ const mockAPI = {
61
+ hap: {
62
+ Service: {},
63
+ Characteristic: {},
64
+ uuid: { generate: jest.fn() },
65
+ },
66
+ on: jest.fn(),
67
+ registerPlatformAccessories: jest.fn(),
68
+ unregisterPlatformAccessories: jest.fn(),
69
+ user: {
70
+ persistPath: () => '/tmp',
71
+ },
72
+ } as any;
73
+
74
+ describe('TuyaPlatform validation around cloud/local modes', () => {
75
+ let log: any;
76
+
77
+ beforeEach(() => {
78
+ log = {
79
+ debug: jest.fn(),
80
+ info: jest.fn(),
81
+ warn: jest.fn(),
82
+ error: jest.fn(),
83
+ };
84
+ jest.clearAllMocks();
85
+ });
86
+
87
+ test('rejects mode both when local block is missing', () => {
88
+ const config = {
89
+ platform: 'TuyaPlatform',
90
+ name: 'Tuya',
91
+ mode: 'both',
92
+ options: {
93
+ projectType: '2',
94
+ accessId: 'id',
95
+ accessKey: 'key',
96
+ countryCode: 1,
97
+ username: 'user@example.com',
98
+ password: 'secret',
99
+ appSchema: 'tuyaSmart',
100
+ generateWeatherAccessory: false,
101
+ weatherAPI: '',
102
+ },
103
+ };
104
+
105
+ const platform = new TuyaPlatform(log, config, mockAPI);
106
+ expect(platform.validate()).toBe(false);
107
+ expect(log.error).toHaveBeenCalledWith('mode is "both" but no "local" config block found.');
108
+ });
109
+
110
+ test('rejects mode both when cloud options are missing', () => {
111
+ const config = {
112
+ platform: 'TuyaPlatform',
113
+ name: 'Tuya',
114
+ mode: 'both',
115
+ local: { devices: [] },
116
+ };
117
+
118
+ const platform = new TuyaPlatform(log, config, mockAPI);
119
+ expect(platform.validate()).toBe(false);
120
+ expect(log.error).toHaveBeenCalledWith('Not configured — "options" block is required for cloud mode, exit.');
121
+ });
122
+
123
+ test('detects duplicate device override ids', () => {
124
+ const config = {
125
+ platform: 'TuyaPlatform',
126
+ name: 'Tuya',
127
+ mode: 'cloud',
128
+ options: {
129
+ projectType: '2',
130
+ accessId: 'id',
131
+ accessKey: 'key',
132
+ countryCode: 1,
133
+ username: 'user@example.com',
134
+ password: 'secret',
135
+ appSchema: 'tuyaSmart',
136
+ generateWeatherAccessory: false,
137
+ weatherAPI: '',
138
+ deviceOverrides: [
139
+ { id: 'same-id' },
140
+ { id: 'same-id' },
141
+ ],
142
+ },
143
+ };
144
+
145
+ const platform = new TuyaPlatform(log, config, mockAPI);
146
+ expect(platform.validateDeviceOverrides()).toBe(false);
147
+ });
148
+
149
+ test('detects duplicate schema codes inside one device override', () => {
150
+ const config = {
151
+ platform: 'TuyaPlatform',
152
+ name: 'Tuya',
153
+ mode: 'cloud',
154
+ options: {
155
+ projectType: '2',
156
+ accessId: 'id',
157
+ accessKey: 'key',
158
+ countryCode: 1,
159
+ username: 'user@example.com',
160
+ password: 'secret',
161
+ appSchema: 'tuyaSmart',
162
+ generateWeatherAccessory: false,
163
+ weatherAPI: '',
164
+ deviceOverrides: [
165
+ {
166
+ id: 'device-1',
167
+ schema: [
168
+ { code: 'switch_1' },
169
+ { code: 'switch_1' },
170
+ ],
171
+ },
172
+ ],
173
+ },
174
+ };
175
+
176
+ const platform = new TuyaPlatform(log, config, mockAPI);
177
+ expect(platform.validateSchema()).toBe(false);
178
+ });
179
+
180
+ test('resolves device config precedence: device, then product, then global', () => {
181
+ const config = {
182
+ platform: 'TuyaPlatform',
183
+ name: 'Tuya',
184
+ mode: 'cloud',
185
+ options: {
186
+ projectType: '2',
187
+ accessId: 'id',
188
+ accessKey: 'key',
189
+ countryCode: 1,
190
+ username: 'user@example.com',
191
+ password: 'secret',
192
+ appSchema: 'tuyaSmart',
193
+ generateWeatherAccessory: false,
194
+ weatherAPI: '',
195
+ deviceOverrides: [
196
+ { id: 'global', category: 'global-cat' },
197
+ { id: 'prod-1', category: 'product-cat' },
198
+ { id: 'device-1', category: 'device-cat' },
199
+ ],
200
+ },
201
+ };
202
+
203
+ const platform = new TuyaPlatform(log, config, mockAPI);
204
+
205
+ const exact = platform.getDeviceConfig({ id: 'device-1', uuid: 'uuid-1', product_id: 'prod-1' } as any);
206
+ const byProduct = platform.getDeviceConfig({ id: 'device-2', uuid: 'uuid-2', product_id: 'prod-1' } as any);
207
+ const fallback = platform.getDeviceConfig({ id: 'device-3', uuid: 'uuid-3', product_id: 'prod-3' } as any);
208
+
209
+ expect(exact?.category).toBe('device-cat');
210
+ expect(byProduct?.category).toBe('product-cat');
211
+ expect(fallback?.category).toBe('global-cat');
212
+ });
213
+
214
+ test('finds schema config case-insensitively and migrates oldCode', () => {
215
+ const config = {
216
+ platform: 'TuyaPlatform',
217
+ name: 'Tuya',
218
+ mode: 'cloud',
219
+ options: {
220
+ projectType: '2',
221
+ accessId: 'id',
222
+ accessKey: 'key',
223
+ countryCode: 1,
224
+ username: 'user@example.com',
225
+ password: 'secret',
226
+ appSchema: 'tuyaSmart',
227
+ generateWeatherAccessory: false,
228
+ weatherAPI: '',
229
+ deviceOverrides: [
230
+ {
231
+ id: 'device-1',
232
+ schema: [
233
+ { oldCode: 'switch_1', code: 'new_switch_1', hidden: true },
234
+ ],
235
+ },
236
+ ],
237
+ },
238
+ };
239
+
240
+ const platform = new TuyaPlatform(log, config, mockAPI);
241
+ const device = { id: 'device-1', uuid: 'device-1', product_id: 'prod-1' } as any;
242
+
243
+ const result = platform.getDeviceSchemaConfig(device, 'SWITCH_1');
244
+
245
+ expect(result).toBeDefined();
246
+ expect(result?.code).toBe('switch_1');
247
+ expect((result as any).newCode).toBe('new_switch_1');
248
+ expect(result?.hidden).toBe(true);
249
+ });
250
+ });
@@ -0,0 +1,446 @@
1
+ /* eslint-disable no-console */
2
+ import { describe, expect, test, beforeEach } from '@jest/globals';
3
+ import { ProtocolFactory } from '../src/local/protocol/ProtocolFactory';
4
+ import { ProtocolV31V32 } from '../src/local/protocol/ProtocolV31V32';
5
+ import { ProtocolV33 } from '../src/local/protocol/ProtocolV33';
6
+ import { ProtocolV34 } from '../src/local/protocol/ProtocolV34';
7
+ import { ProtocolV35 } from '../src/local/protocol/ProtocolV35';
8
+ import {
9
+ packMessage55AA, unpackMessage55AA,
10
+ packMessage6699, unpackMessage6699,
11
+ hmac, encryptECB, encryptECBNoPad,
12
+ NO_VERSION_HEADER_CMDS,
13
+ } from '../src/local/protocol/ProtocolUtilities';
14
+
15
+ describe('ProtocolFactory', () => {
16
+ test('creates ProtocolV31V32 for version 3.1', () => {
17
+ const protocol = ProtocolFactory.createProtocol('3.1');
18
+ expect(protocol).toBeInstanceOf(ProtocolV31V32);
19
+ });
20
+
21
+ test('creates ProtocolV31V32 for version 3.2', () => {
22
+ const protocol = ProtocolFactory.createProtocol('3.2');
23
+ expect(protocol).toBeInstanceOf(ProtocolV31V32);
24
+ });
25
+
26
+ test('creates ProtocolV33 for version 3.3', () => {
27
+ const protocol = ProtocolFactory.createProtocol('3.3');
28
+ expect(protocol).toBeInstanceOf(ProtocolV33);
29
+ });
30
+
31
+ test('creates ProtocolV34 for version 3.4', () => {
32
+ const protocol = ProtocolFactory.createProtocol('3.4');
33
+ expect(protocol).toBeInstanceOf(ProtocolV34);
34
+ });
35
+
36
+ test('creates ProtocolV35 for version 3.5', () => {
37
+ const protocol = ProtocolFactory.createProtocol('3.5');
38
+ expect(protocol).toBeInstanceOf(ProtocolV35);
39
+ });
40
+
41
+ test('throws error for unsupported version', () => {
42
+ expect(() => ProtocolFactory.createProtocol('4.0')).toThrow(
43
+ 'Unsupported protocol version: 4.0'
44
+ );
45
+ });
46
+
47
+ test('throws error for invalid version format', () => {
48
+ expect(() => ProtocolFactory.createProtocol('invalid')).toThrow(
49
+ 'Unsupported protocol version: invalid'
50
+ );
51
+ });
52
+ });
53
+
54
+ describe('Protocol Base Interface', () => {
55
+ let protocol: ReturnType<typeof ProtocolFactory.createProtocol>;
56
+
57
+ beforeEach(() => {
58
+ protocol = ProtocolFactory.createProtocol('3.5');
59
+ });
60
+
61
+ describe('encodeFrame', () => {
62
+ test('encodes frame with valid command and data', () => {
63
+ const cmd = 0x01;
64
+ const data = Buffer.from([0xaa, 0xbb, 0xcc]);
65
+ const seqNo = 1;
66
+
67
+ const frame = protocol.encodeFrame(cmd, data, seqNo);
68
+
69
+ expect(frame).toBeInstanceOf(Buffer);
70
+ expect(frame.length).toBeGreaterThan(0);
71
+ });
72
+
73
+ test('encodes frame with session key', () => {
74
+ const cmd = 0x01;
75
+ const data = Buffer.from([0xaa, 0xbb, 0xcc]);
76
+ const seqNo = 1;
77
+ const sessionKey = Buffer.from('0123456789abcdef'); // 16 bytes - correct for AES-128-GCM
78
+
79
+ const frame = protocol.encodeFrame(cmd, data, seqNo, sessionKey);
80
+
81
+ expect(frame).toBeInstanceOf(Buffer);
82
+ expect(frame.length).toBeGreaterThan(0);
83
+ });
84
+
85
+ test('encodes empty data', () => {
86
+ const cmd = 0x01;
87
+ const data = Buffer.alloc(0);
88
+ const seqNo = 1;
89
+
90
+ const frame = protocol.encodeFrame(cmd, data, seqNo);
91
+
92
+ expect(frame).toBeInstanceOf(Buffer);
93
+ expect(frame.length).toBeGreaterThan(0);
94
+ });
95
+ });
96
+
97
+ describe('isFrameComplete', () => {
98
+ test('identifies complete frame', () => {
99
+ // Frame with proper structure should be identified as complete
100
+ const cmd = 0x01;
101
+ const data = Buffer.from([0xaa, 0xbb]);
102
+ const seqNo = 1;
103
+
104
+ const frame = protocol.encodeFrame(cmd, data, seqNo);
105
+ const isComplete = protocol.isFrameComplete(frame);
106
+
107
+ expect(typeof isComplete).toBe('boolean');
108
+ });
109
+
110
+ test('handles empty buffer', () => {
111
+ const buffer = Buffer.alloc(0);
112
+ const isComplete = protocol.isFrameComplete(buffer);
113
+
114
+ expect(typeof isComplete).toBe('boolean');
115
+ expect(isComplete).toBe(false);
116
+ });
117
+
118
+ test('handles incomplete buffer', () => {
119
+ const buffer = Buffer.from([0x00, 0x00]);
120
+ const isComplete = protocol.isFrameComplete(buffer);
121
+
122
+ expect(typeof isComplete).toBe('boolean');
123
+ });
124
+ });
125
+
126
+ describe('extractFrame', () => {
127
+ test('extracts valid frame', () => {
128
+ const cmd = 0x01;
129
+ const data = Buffer.from([0xaa, 0xbb]);
130
+ const seqNo = 1;
131
+
132
+ const encodedFrame = protocol.encodeFrame(cmd, data, seqNo);
133
+ const result = protocol.extractFrame(encodedFrame);
134
+
135
+ if (result) {
136
+ expect(result.frame).toBeInstanceOf(Buffer);
137
+ expect(result.remaining).toBeInstanceOf(Buffer);
138
+ }
139
+ });
140
+
141
+ test('handles incomplete frame gracefully', () => {
142
+ const buffer = Buffer.from([0x00, 0x00, 0x55]);
143
+ const result = protocol.extractFrame(buffer);
144
+
145
+ if (result === null) {
146
+ expect(result).toBeNull();
147
+ }
148
+ });
149
+
150
+ test('handles empty buffer', () => {
151
+ const buffer = Buffer.alloc(0);
152
+ const result = protocol.extractFrame(buffer);
153
+
154
+ expect(result).toBeNull();
155
+ });
156
+ });
157
+ });
158
+
159
+ describe('Protocol Version Specific Behaviors', () => {
160
+ test('ProtocolV31V32 creates valid frames', () => {
161
+ const protocol = new ProtocolV31V32();
162
+ const cmd = 0x01;
163
+ const data = Buffer.from('test');
164
+ const seqNo = 1;
165
+
166
+ const frame = protocol.encodeFrame(cmd, data, seqNo);
167
+
168
+ expect(frame).toBeInstanceOf(Buffer);
169
+ expect(frame.length).toBeGreaterThan(0);
170
+ });
171
+
172
+ test('ProtocolV33 creates valid frames', () => {
173
+ const protocol = new ProtocolV33();
174
+ const cmd = 0x01;
175
+ const data = Buffer.from('test');
176
+ const seqNo = 1;
177
+
178
+ const frame = protocol.encodeFrame(cmd, data, seqNo);
179
+
180
+ expect(frame).toBeInstanceOf(Buffer);
181
+ expect(frame.length).toBeGreaterThan(0);
182
+ });
183
+
184
+ test('ProtocolV34 creates valid frames', () => {
185
+ const protocol = new ProtocolV34();
186
+ const cmd = 0x01;
187
+ const data = Buffer.from('test');
188
+ const seqNo = 1;
189
+
190
+ const frame = protocol.encodeFrame(cmd, data, seqNo);
191
+
192
+ expect(frame).toBeInstanceOf(Buffer);
193
+ expect(frame.length).toBeGreaterThan(0);
194
+ });
195
+
196
+ test('ProtocolV35 creates valid frames with IV', () => {
197
+ const protocol = new ProtocolV35();
198
+ const cmd = 0x01;
199
+ const data = Buffer.from('test');
200
+ const seqNo = 1;
201
+ const sessionKey = Buffer.from('0123456789abcdef'); // 16 bytes - correct for AES-128-GCM
202
+
203
+ const frame = protocol.encodeFrame(cmd, data, seqNo, sessionKey);
204
+
205
+ expect(frame).toBeInstanceOf(Buffer);
206
+ expect(frame.length).toBeGreaterThan(0);
207
+ });
208
+ });
209
+
210
+ // ── Round-trip encode/decode tests ────────────────────────────────────────────
211
+
212
+ describe('packMessage55AA / unpackMessage55AA round-trips', () => {
213
+ const key = Buffer.from('0123456789abcdef');
214
+
215
+ test('CRC32 frame round-trip (noRetcode=true)', () => {
216
+ const payload = Buffer.from('{"dps":{"1":true}}');
217
+ const frame = packMessage55AA(7, 10, payload);
218
+ const msg = unpackMessage55AA(frame, undefined, true);
219
+ expect(msg).not.toBeNull();
220
+ expect(msg!.seqno).toBe(7);
221
+ expect(msg!.cmd).toBe(10);
222
+ expect(msg!.payload).toEqual(payload);
223
+ expect(msg!.hmacOk).toBe(true);
224
+ });
225
+
226
+ test('HMAC-SHA256 frame round-trip (noRetcode=true)', () => {
227
+ const payload = Buffer.from('abcdef0123456789abcdef0123456789'); // 32 bytes
228
+ const frame = packMessage55AA(3, 3, payload, key);
229
+ const msg = unpackMessage55AA(frame, key, true);
230
+ expect(msg).not.toBeNull();
231
+ expect(msg!.seqno).toBe(3);
232
+ expect(msg!.cmd).toBe(3);
233
+ expect(msg!.payload).toEqual(payload);
234
+ expect(msg!.hmacOk).toBe(true);
235
+ });
236
+
237
+ test('HMAC frame fails verification with wrong key', () => {
238
+ const wrongKey = Buffer.from('fedcba9876543210');
239
+ const frame = packMessage55AA(1, 13, Buffer.from('test'), key);
240
+ const msg = unpackMessage55AA(frame, wrongKey, true);
241
+ expect(msg).not.toBeNull();
242
+ expect(msg!.hmacOk).toBe(false);
243
+ });
244
+ });
245
+
246
+ describe('packMessage6699 / unpackMessage6699 round-trips', () => {
247
+ const key = Buffer.from('0123456789abcdef');
248
+
249
+ test('GCM frame round-trip preserves seqno, cmd, and payload', () => {
250
+ const plaintext = Buffer.from('{"dps":{"1":true}}');
251
+ const frame = packMessage6699(42, 13, plaintext, key);
252
+ const msg = unpackMessage6699(frame, key);
253
+ expect(msg).not.toBeNull();
254
+ expect(msg!.seqno).toBe(42);
255
+ expect(msg!.cmd).toBe(13);
256
+ expect(msg!.hmacOk).toBe(true);
257
+ // Payload starts with '{' (not 0x00 0x00), so retcode stripping does not apply
258
+ expect(msg!.payload).toEqual(plaintext);
259
+ });
260
+
261
+ test('returns null for wrong decryption key (GCM auth tag mismatch)', () => {
262
+ const wrongKey = Buffer.from('fedcba9876543210');
263
+ const frame = packMessage6699(1, 1, Buffer.from('hello'), key);
264
+ const msg = unpackMessage6699(frame, wrongKey);
265
+ expect(msg).toBeNull();
266
+ });
267
+
268
+ test('frame length field is consistent with frame byte count', () => {
269
+ const plaintext = Buffer.from('x'.repeat(37)); // odd-length to catch off-by-one
270
+ const frame = packMessage6699(1, 16, plaintext, key);
271
+ // header=18, iv=12, ciphertext=37, tag=16, suffix=4 → total 87 bytes
272
+ expect(frame.length).toBe(18 + 12 + 37 + 16 + 4);
273
+ // length field at offset 14 should be 12+37+16+4 = 69
274
+ expect(frame.readUInt32BE(14)).toBe(12 + 37 + 16 + 4);
275
+ });
276
+ });
277
+
278
+ describe('ProtocolV35 encode + decode round-trips', () => {
279
+ const key = Buffer.from('0123456789abcdef');
280
+ const proto = new ProtocolV35();
281
+
282
+ test('data command (cmd 13) with session key: round-trip strips version header', () => {
283
+ const payload = Buffer.from('{"protocol":5,"t":1234,"data":{"dps":{"1":true}}}');
284
+ const frame = proto.encodeFrame(13, payload, 1, key);
285
+ const decoded = proto.decodeFrame(frame, key, key);
286
+ expect(decoded).not.toBeNull();
287
+ expect(decoded!.cmd).toBe(13);
288
+ expect(decoded!.payload).toEqual(payload);
289
+ });
290
+
291
+ test('key exchange cmd 3 (NO_VERSION_HEADER_CMDS): no version header added or stripped', () => {
292
+ const nonce = Buffer.from('0123456789abcdef'); // 16 bytes
293
+ const frame = proto.encodeFrame(3, nonce, 1, key);
294
+ const decoded = proto.decodeFrame(frame, key, key);
295
+ expect(decoded).not.toBeNull();
296
+ expect(decoded!.cmd).toBe(3);
297
+ expect(decoded!.payload).toEqual(nonce); // exactly 16 bytes, no header stripped
298
+ });
299
+
300
+ test('decodeFrame falls back to deviceKey when sessionKey is absent (pre-exchange)', () => {
301
+ const nonce = Buffer.from('0123456789abcdef');
302
+ // Encode with key (simulating device using deviceKey before exchange)
303
+ const frame = proto.encodeFrame(3, nonce, 1, key);
304
+ // Decode without passing sessionKey - should use deviceKey as fallback
305
+ const decoded = proto.decodeFrame(frame, key); // sessionKey omitted
306
+ expect(decoded).not.toBeNull();
307
+ expect(decoded!.payload).toEqual(nonce);
308
+ });
309
+
310
+ test('heartbeat cmd 9 (NO_VERSION_HEADER_CMDS): payload preserved', () => {
311
+ const ping = Buffer.from('{}');
312
+ const frame = proto.encodeFrame(9, ping, 5, key);
313
+ const decoded = proto.decodeFrame(frame, key, key);
314
+ expect(decoded).not.toBeNull();
315
+ expect(decoded!.cmd).toBe(9);
316
+ expect(decoded!.payload).toEqual(ping);
317
+ });
318
+ });
319
+
320
+ describe('ProtocolV34 encode + decode round-trips', () => {
321
+ const key = Buffer.from('0123456789abcdef');
322
+ const proto = new ProtocolV34();
323
+
324
+ test('simulated device response (cmd 16, DP_QUERY_NEW) with session key: no version header', () => {
325
+ // DP_QUERY_NEW is in NO_VERSION_HEADER_CMDS — device sends no version header prefix
326
+ const json = Buffer.from('{"dps":{"1":true}}');
327
+ const encrypted = encryptECB(json, key);
328
+ const retcode = Buffer.alloc(4);
329
+ const rawWirePayload = Buffer.concat([retcode, encrypted]);
330
+ const frame = packMessage55AA(5, 16, rawWirePayload, key);
331
+
332
+ const decoded = proto.decodeFrame(frame, key, key);
333
+ expect(decoded).not.toBeNull();
334
+ expect(decoded!.cmd).toBe(16);
335
+ expect(decoded!.payload.toString()).toBe('{"dps":{"1":true}}');
336
+ });
337
+
338
+ test('simulated device response (cmd 8, STATUS) with session key: version header stripped', () => {
339
+ // STATUS (cmd 8) is NOT in NO_VERSION_HEADER_CMDS — device includes "3.4\x00..." prefix
340
+ const json = Buffer.from('{"dps":{"1":true}}');
341
+ const versionHeader = Buffer.from('3.4' + '\x00'.repeat(12), 'latin1');
342
+ const encrypted = encryptECB(Buffer.concat([versionHeader, json]), key);
343
+ const retcode = Buffer.alloc(4);
344
+ const rawWirePayload = Buffer.concat([retcode, encrypted]);
345
+ const frame = packMessage55AA(5, 8, rawWirePayload, key);
346
+
347
+ const decoded = proto.decodeFrame(frame, key, key);
348
+ expect(decoded).not.toBeNull();
349
+ expect(decoded!.cmd).toBe(8);
350
+ expect(decoded!.payload.toString()).toBe('{"dps":{"1":true}}');
351
+ });
352
+
353
+ test('key exchange cmd 4 response: retcode + encrypted nonce+hmac decoded correctly', () => {
354
+ const localNonce = Buffer.from('0123456789abcdef');
355
+ const remoteNonce = Buffer.from('fedcba9876543210');
356
+ const hmacVal = hmac(localNonce, key); // 32 bytes
357
+ // Device sends: retcode(4) + ECB_encrypt(remoteNonce(16) + hmac(32), deviceKey)
358
+ const plaintext = Buffer.concat([remoteNonce, hmacVal]); // 48 bytes
359
+ const encrypted = encryptECB(plaintext, key);
360
+ const retcode = Buffer.alloc(4);
361
+ const rawWirePayload = Buffer.concat([retcode, encrypted]);
362
+ const frame = packMessage55AA(2, 4, rawWirePayload, key);
363
+
364
+ const decoded = proto.decodeFrame(frame, key); // no sessionKey (pre-exchange)
365
+ expect(decoded).not.toBeNull();
366
+ expect(decoded!.cmd).toBe(4);
367
+ // The decrypted payload should be the 48-byte plaintext (cmd 4 is NO_VERSION_HEADER, no stripping)
368
+ expect(decoded!.payload).toEqual(plaintext);
369
+ });
370
+
371
+ test('processKeyExchangeStep2 derives session key and step3 HMAC', () => {
372
+ const localNonce = Buffer.from('0123456789abcdef');
373
+ const remoteNonce = Buffer.from('fedcba9876543210');
374
+ const hmacVal = hmac(localNonce, key);
375
+ const step2Payload = Buffer.concat([remoteNonce, hmacVal]);
376
+
377
+ const result = proto.processKeyExchangeStep2(step2Payload, localNonce, key);
378
+
379
+ expect(result).not.toBeNull();
380
+ // Step3 = HMAC(remoteNonce, realKey)
381
+ expect(result!.step3Payload).toEqual(hmac(remoteNonce, key));
382
+ // SessionKey = ECBNoPad(localXORremote, realKey)
383
+ const xored = Buffer.allocUnsafe(16);
384
+ for (let i = 0; i < 16; i++) xored[i] = localNonce[i] ^ remoteNonce[i];
385
+ expect(result!.sessionKey).toEqual(encryptECBNoPad(xored, key));
386
+ });
387
+
388
+ test('processKeyExchangeStep2 returns null on HMAC mismatch', () => {
389
+ const localNonce = Buffer.from('0123456789abcdef');
390
+ const remoteNonce = Buffer.from('fedcba9876543210');
391
+ const badHmac = Buffer.alloc(32); // all zeros – wrong
392
+ const step2Payload = Buffer.concat([remoteNonce, badHmac]);
393
+
394
+ const result = proto.processKeyExchangeStep2(step2Payload, localNonce, key);
395
+ expect(result).toBeNull();
396
+ });
397
+ });
398
+
399
+ describe('ProtocolV35 key exchange', () => {
400
+ const key = Buffer.from('0123456789abcdef');
401
+ const proto = new ProtocolV35();
402
+
403
+ test('processKeyExchangeStep2 derives session key via GCM (v3.5 path)', () => {
404
+ const localNonce = Buffer.from('0123456789abcdef');
405
+ const remoteNonce = Buffer.from('fedcba9876543210');
406
+ const hmacVal = hmac(localNonce, key);
407
+ const step2Payload = Buffer.concat([remoteNonce, hmacVal]);
408
+
409
+ const result = proto.processKeyExchangeStep2(step2Payload, localNonce, key);
410
+
411
+ expect(result).not.toBeNull();
412
+ expect(result!.step3Payload).toEqual(hmac(remoteNonce, key));
413
+ expect(result!.sessionKey.length).toBe(16);
414
+ // Verify v3.5 session key differs from v3.4 session key (different derivation)
415
+ const proto34 = new ProtocolV34();
416
+ const result34 = proto34.processKeyExchangeStep2(step2Payload, localNonce, key);
417
+ expect(result!.sessionKey).not.toEqual(result34!.sessionKey);
418
+ });
419
+ });
420
+
421
+ describe('NO_VERSION_HEADER_CMDS coverage', () => {
422
+ test('set contains expected key exchange and query commands', () => {
423
+ expect(NO_VERSION_HEADER_CMDS.has(3)).toBe(true); // SESS_KEY_NEG_START
424
+ expect(NO_VERSION_HEADER_CMDS.has(4)).toBe(true); // SESS_KEY_NEG_RESP
425
+ expect(NO_VERSION_HEADER_CMDS.has(5)).toBe(true); // SESS_KEY_NEG_FINISH
426
+ expect(NO_VERSION_HEADER_CMDS.has(9)).toBe(true); // HEART_BEAT
427
+ expect(NO_VERSION_HEADER_CMDS.has(0x10)).toBe(true); // DP_QUERY_NEW
428
+ expect(NO_VERSION_HEADER_CMDS.has(13)).toBe(false); // CONTROL_NEW gets header
429
+ expect(NO_VERSION_HEADER_CMDS.has(7)).toBe(false); // CONTROL gets header
430
+ });
431
+
432
+ test('ProtocolV34: cmd 3 payload decoded without version header stripping', () => {
433
+ // A nonce whose first byte happens to be 0x33 ('3') — must NOT be stripped
434
+ const trickyNonce = Buffer.from('3456789012345678'); // starts with '3'
435
+ const encrypted = encryptECB(trickyNonce, Buffer.from('0123456789abcdef'));
436
+ const retcode = Buffer.alloc(4);
437
+ const frame = packMessage55AA(1, 3, Buffer.concat([retcode, encrypted]), Buffer.from('0123456789abcdef'));
438
+
439
+ const proto = new ProtocolV34();
440
+ const decoded = proto.decodeFrame(frame, Buffer.from('0123456789abcdef'));
441
+ expect(decoded).not.toBeNull();
442
+ // length must be 16 (nonce), not 1 (nonce minus 15-byte header strip)
443
+ expect(decoded!.payload.length).toBe(16);
444
+ expect(decoded!.payload).toEqual(trickyNonce);
445
+ });
446
+ });