@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,616 @@
1
+ /// <reference types="node" />
2
+ /* eslint-disable no-console */
3
+ import { describe, expect, test, beforeEach, afterEach, jest } from '@jest/globals';
4
+ import EventEmitter from 'events';
5
+ import LocalDevice, { LocalDeviceContext } from '../src/local/LocalDevice';
6
+
7
+ // Mock Logger
8
+ jest.mock('../src/shared/util/Logger', () => ({
9
+ __esModule: true,
10
+ default: class Logger {
11
+ log() {}
12
+ info() {}
13
+ warn() {}
14
+ error() {}
15
+ debug() {}
16
+ },
17
+ PrefixLogger: class PrefixLogger {
18
+ constructor(public log: any, public prefix: string, public debugMode: boolean) {}
19
+ debug(message?: any, ...args: any[]) {
20
+ if (this.debugMode) {
21
+ this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);
22
+ } else {
23
+ this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);
24
+ }
25
+ }
26
+ info(message?: any, ...args: any[]) {
27
+ this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);
28
+ }
29
+ warn(message?: any, ...args: any[]) {
30
+ this.log.warn((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);
31
+ }
32
+ error(message?: any, ...args: any[]) {
33
+ this.log.error((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);
34
+ }
35
+ },
36
+ }));
37
+
38
+ // Mock Protocol
39
+ const mockProtocol = {
40
+ frameSize: jest.fn(() => 0),
41
+ isCompleteFrame: jest.fn(() => false),
42
+ extractFrames: jest.fn(() => []),
43
+ };
44
+ jest.mock('../src/local/protocol/ProtocolFactory', () => ({
45
+ ProtocolFactory: {
46
+ createProtocol: jest.fn(() => mockProtocol),
47
+ },
48
+ }));
49
+
50
+ // Mock net module partially
51
+ let mockSocket: any;
52
+ jest.mock('net', () => ({
53
+ Socket: class MockSocket extends EventEmitter {
54
+ connect = jest.fn();
55
+ write = jest.fn();
56
+ destroy = jest.fn();
57
+ },
58
+ createConnection: jest.fn(() => mockSocket),
59
+ }));
60
+
61
+ describe('LocalDevice', () => {
62
+ let device: LocalDevice;
63
+ let mockLogger: any;
64
+ let context: LocalDeviceContext;
65
+
66
+ beforeEach(() => {
67
+ mockLogger = {
68
+ log: jest.fn(),
69
+ info: jest.fn(),
70
+ warn: jest.fn(),
71
+ error: jest.fn(),
72
+ debug: jest.fn(),
73
+ };
74
+
75
+ context = {
76
+ id: 'device_001',
77
+ key: Buffer.from('0123456789abcdef'),
78
+ ip: '192.168.1.100',
79
+ version: '3.5',
80
+ name: 'Test Device',
81
+ port: 6668,
82
+ pingGap: 9,
83
+ connectTimeout: 30,
84
+ };
85
+
86
+ mockSocket = new EventEmitter();
87
+ mockSocket.connect = jest.fn();
88
+ mockSocket.write = jest.fn();
89
+ mockSocket.destroy = jest.fn();
90
+ mockSocket.removeAllListeners = jest.fn();
91
+
92
+ device = new LocalDevice(context, mockLogger);
93
+ });
94
+
95
+ afterEach(() => {
96
+ if (device) {
97
+ device.disconnect();
98
+ }
99
+ });
100
+
101
+ describe('initialization', () => {
102
+ test('creates device with context', () => {
103
+ expect(device).toBeInstanceOf(EventEmitter);
104
+ expect(device.connected).toBe(false);
105
+ expect(device.state).toEqual({});
106
+ });
107
+
108
+ test('sets default port if not provided', () => {
109
+ const contextNoPort = { ...context, port: undefined };
110
+ const dev = new LocalDevice(contextNoPort, mockLogger);
111
+
112
+ expect(dev).toBeDefined();
113
+ expect(dev.connected).toBe(false);
114
+ });
115
+
116
+ test('sets default pingGap if not provided', () => {
117
+ const contextNoPingGap = { ...context, pingGap: undefined };
118
+ const dev = new LocalDevice(contextNoPingGap, mockLogger);
119
+
120
+ expect(dev).toBeDefined();
121
+ });
122
+
123
+ test('sets default connectTimeout if not provided', () => {
124
+ const contextNoTimeout = { ...context, connectTimeout: undefined };
125
+ const dev = new LocalDevice(contextNoTimeout, mockLogger);
126
+
127
+ expect(dev).toBeDefined();
128
+ });
129
+
130
+ test('creates logger with device name', () => {
131
+ expect(device.log).toBeDefined();
132
+ });
133
+ });
134
+
135
+ describe('connection state', () => {
136
+ test('starts in disconnected state', () => {
137
+ expect(device.connected).toBe(false);
138
+ });
139
+
140
+ test('initializes empty state object', () => {
141
+ expect(device.state).toEqual({});
142
+ expect(typeof device.state).toBe('object');
143
+ });
144
+
145
+ test('is an EventEmitter', () => {
146
+ expect(device instanceof EventEmitter).toBe(true);
147
+ expect(typeof device.on).toBe('function');
148
+ expect(typeof device.emit).toBe('function');
149
+ });
150
+ });
151
+
152
+ describe('disconnect', () => {
153
+ test('disconnects and clears socket', () => {
154
+ device['socket'] = mockSocket;
155
+ device['connected'] = true;
156
+ device['sessionKey'] = Buffer.from('test_key');
157
+
158
+ device.disconnect();
159
+
160
+ expect(mockSocket.removeAllListeners).toHaveBeenCalled();
161
+ expect(mockSocket.destroy).toHaveBeenCalled();
162
+ expect(device.connected).toBe(false);
163
+ });
164
+
165
+ test('handles disconnect when socket is undefined', () => {
166
+ device['socket'] = undefined;
167
+ device.disconnect();
168
+
169
+ expect(device.connected).toBe(false);
170
+ });
171
+
172
+ test('clears session key on disconnect', () => {
173
+ device['socket'] = mockSocket;
174
+ device['sessionKey'] = Buffer.from('test');
175
+ device['connected'] = true;
176
+
177
+ device.disconnect();
178
+
179
+ expect(device['sessionKey']).toBeUndefined();
180
+ });
181
+ });
182
+
183
+ describe('state management', () => {
184
+ test('allows setting state directly', () => {
185
+ device.state = { switch: 1, brightness: 100 };
186
+
187
+ expect((device.state as any).switch).toBe(1);
188
+ expect((device.state as any).brightness).toBe(100);
189
+ });
190
+
191
+ test('state persists between operations', () => {
192
+ device.state['test_key'] = 'test_value';
193
+ const value1 = device.state['test_key'];
194
+
195
+ device.state['another_key'] = 'another_value';
196
+ const value2 = device.state['test_key'];
197
+
198
+ expect(value1).toBe(value2);
199
+ });
200
+
201
+ test('can reset state', () => {
202
+ device.state = { key1: 'value1', key2: 'value2' };
203
+ device.state = {};
204
+
205
+ expect(device.state).toEqual({});
206
+ });
207
+
208
+ test('handles complex state objects', () => {
209
+ device.state = {
210
+ switch: true,
211
+ brightness: 100,
212
+ color: { r: 255, g: 0, b: 0 },
213
+ modes: ['mode1', 'mode2'],
214
+ };
215
+
216
+ expect((device.state as any).switch).toBe(true);
217
+ expect((device.state as any).color.r).toBe(255);
218
+ expect((device.state as any).modes).toEqual(['mode1', 'mode2']);
219
+ });
220
+ });
221
+
222
+ describe('event emitter behavior', () => {
223
+ test('emits custom events', (done) => {
224
+ device.on('test_event', (data) => {
225
+ expect(data).toBe('test_data');
226
+ done();
227
+ });
228
+
229
+ device.emit('test_event', 'test_data');
230
+ });
231
+
232
+ test('supports multiple listeners', (done) => {
233
+ let count = 0;
234
+ const increment = () => {
235
+ count++;
236
+ if (count === 2) {
237
+ expect(count).toBe(2);
238
+ done();
239
+ }
240
+ };
241
+
242
+ device.on('event', increment);
243
+ device.on('event', increment);
244
+ device.emit('event');
245
+ });
246
+
247
+ test('supports once listener', (done) => {
248
+ let count = 0;
249
+ device.once('once_event', () => {
250
+ count++;
251
+ });
252
+
253
+ device.emit('once_event');
254
+ device.emit('once_event');
255
+
256
+ // Give it a moment to settle
257
+ setImmediate(() => {
258
+ expect(count).toBe(1);
259
+ done();
260
+ });
261
+ });
262
+
263
+ test('removes listeners', (done) => {
264
+ const listener = jest.fn();
265
+ device.on('test', listener);
266
+ device.removeListener('test', listener);
267
+
268
+ device.emit('test', 'data');
269
+
270
+ setImmediate(() => {
271
+ expect(listener).not.toHaveBeenCalled();
272
+ done();
273
+ });
274
+ });
275
+ });
276
+
277
+ describe('context properties', () => {
278
+ test('stores device id', () => {
279
+ expect(device['context'].id).toBe('device_001');
280
+ });
281
+
282
+ test('stores device IP', () => {
283
+ expect(device['context'].ip).toBe('192.168.1.100');
284
+ });
285
+
286
+ test('stores device key', () => {
287
+ expect(device['context'].key).toEqual(Buffer.from('0123456789abcdef'));
288
+ });
289
+
290
+ test('stores protocol version', () => {
291
+ expect(device['context'].version).toBe('3.5');
292
+ });
293
+
294
+ test('stores device name', () => {
295
+ expect(device['context'].name).toBe('Test Device');
296
+ });
297
+ });
298
+
299
+ describe('protocol handling', () => {
300
+ test('creates protocol for version 3.5', () => {
301
+ const ctxV35 = { ...context, version: '3.5' };
302
+ const devV35 = new LocalDevice(ctxV35, mockLogger);
303
+
304
+ expect(devV35['protocol']).toBeDefined();
305
+ });
306
+
307
+ test('creates protocol for version 3.1', () => {
308
+ const ctxV31 = { ...context, version: '3.1' };
309
+ const devV31 = new LocalDevice(ctxV31, mockLogger);
310
+
311
+ expect(devV31['protocol']).toBeDefined();
312
+ });
313
+
314
+ test('creates protocol for version 3.4', () => {
315
+ const ctxV34 = { ...context, version: '3.4' };
316
+ const devV34 = new LocalDevice(ctxV34, mockLogger);
317
+
318
+ expect(devV34['protocol']).toBeDefined();
319
+ });
320
+ });
321
+
322
+ describe('private state tracking', () => {
323
+ test('initializes connection attempts to 0', () => {
324
+ expect(device['connectionAttempts']).toBe(0);
325
+ });
326
+
327
+ test('initializes send counter to 0', () => {
328
+ expect(device['sendCounter']).toBe(0);
329
+ });
330
+
331
+ test('initializes cached buffer', () => {
332
+ expect(device['cachedBuffer']).toBeDefined();
333
+ expect(device['cachedBuffer'].length).toBe(0);
334
+ });
335
+
336
+ test('initializes without session key', () => {
337
+ expect(device['sessionKey']).toBeUndefined();
338
+ });
339
+
340
+ test('initializes without temp keys', () => {
341
+ expect(device['tmpLocalKey']).toBeUndefined();
342
+ });
343
+ });
344
+
345
+ describe('log prefix configuration', () => {
346
+ test('creates device logger', () => {
347
+ expect(device.log).toBeDefined();
348
+ });
349
+
350
+ test('creates device logger with context', () => {
351
+ const ctxNoName = { ...context, name: undefined };
352
+ const devNoName = new LocalDevice(ctxNoName, mockLogger);
353
+
354
+ expect(devNoName.log).toBeDefined();
355
+ });
356
+ });
357
+
358
+ describe('connection configuration', () => {
359
+ test('uses custom port if provided', () => {
360
+ const ctxCustomPort = { ...context, port: 8888 };
361
+ const devCustomPort = new LocalDevice(ctxCustomPort, mockLogger);
362
+
363
+ expect(devCustomPort['context'].port).toBe(8888);
364
+ });
365
+
366
+ test('uses custom pingGap if provided', () => {
367
+ const ctxCustomPing = { ...context, pingGap: 15 };
368
+ const devCustomPing = new LocalDevice(ctxCustomPing, mockLogger);
369
+
370
+ expect(devCustomPing['context'].pingGap).toBe(15);
371
+ });
372
+
373
+ test('uses custom connectTimeout if provided', () => {
374
+ const ctxCustomTimeout = { ...context, connectTimeout: 60 };
375
+ const devCustomTimeout = new LocalDevice(ctxCustomTimeout, mockLogger);
376
+
377
+ expect(devCustomTimeout['context'].connectTimeout).toBe(60);
378
+ });
379
+ });
380
+
381
+ describe('device types', () => {
382
+ test('handles light device', () => {
383
+ const lightContext = { ...context, id: 'light_001', name: 'Smart Light' };
384
+ const light = new LocalDevice(lightContext, mockLogger);
385
+
386
+ expect(light['context'].id).toBe('light_001');
387
+ expect(light['context'].name).toBe('Smart Light');
388
+ });
389
+
390
+ test('handles switch device', () => {
391
+ const switchContext = { ...context, id: 'switch_001', name: 'Smart Switch' };
392
+ const switchDev = new LocalDevice(switchContext, mockLogger);
393
+
394
+ expect(switchDev['context'].id).toBe('switch_001');
395
+ });
396
+
397
+ test('handles outlet device', () => {
398
+ const outletContext = { ...context, id: 'outlet_001', name: 'Smart Outlet' };
399
+ const outlet = new LocalDevice(outletContext, mockLogger);
400
+
401
+ expect(outlet['context'].id).toBe('outlet_001');
402
+ });
403
+ });
404
+
405
+ describe('key exchange - HMAC verification (FIX: use context.key not sessionKey)', () => {
406
+ let deviceV34: LocalDevice;
407
+ let deviceV35: LocalDevice;
408
+ let contextV34: LocalDeviceContext;
409
+ let contextV35: LocalDeviceContext;
410
+
411
+ beforeEach(() => {
412
+ contextV34 = {
413
+ id: 'device_v34',
414
+ key: Buffer.from('0123456789abcdef'),
415
+ ip: '192.168.1.100',
416
+ version: '3.4',
417
+ name: 'Test Device v3.4',
418
+ port: 6668,
419
+ connectTimeout: 30,
420
+ };
421
+
422
+ contextV35 = {
423
+ id: 'device_v35',
424
+ key: Buffer.from('fedcba9876543210'),
425
+ ip: '192.168.1.101',
426
+ version: '3.5',
427
+ name: 'Test Device v3.5',
428
+ port: 6668,
429
+ connectTimeout: 30,
430
+ };
431
+
432
+ deviceV34 = new LocalDevice(contextV34, mockLogger);
433
+ deviceV35 = new LocalDevice(contextV35, mockLogger);
434
+ });
435
+
436
+ afterEach(() => {
437
+ deviceV34.disconnect();
438
+ deviceV35.disconnect();
439
+ });
440
+
441
+ test('accepts valid HMAC in key exchange step 2 (v3.4)', () => {
442
+ // Simulate receiving key exchange step 2 response
443
+ const { hmac } = require('../src/local/protocol/ProtocolUtilities');
444
+ const localNonce = Buffer.from('0123456789abcdef');
445
+ const remoteNonce = Buffer.from('fedcba9876543210');
446
+ const validHmac = hmac(localNonce, contextV34.key);
447
+ const step2Payload = Buffer.concat([remoteNonce, validHmac]);
448
+
449
+ // Set up device state for key exchange
450
+ deviceV34['tmpLocalKey'] = localNonce;
451
+ deviceV34['socket'] = mockSocket;
452
+
453
+ const connectSpy = jest.spyOn(deviceV34, 'emit');
454
+ deviceV34['_handleKeyExchangeResponse'](step2Payload);
455
+
456
+ // Should emit 'connect' and set connected state
457
+ expect(deviceV34.connected).toBe(true);
458
+ expect(connectSpy).toHaveBeenCalledWith('connect');
459
+ });
460
+
461
+ test('rejects invalid HMAC in key exchange step 2 (v3.4)', () => {
462
+ // Use wrong HMAC that doesn't match
463
+ const localNonce = Buffer.from('0123456789abcdef');
464
+ const remoteNonce = Buffer.from('fedcba9876543210');
465
+ const invalidHmac = Buffer.alloc(32); // all zeros - definitely wrong
466
+ const step2Payload = Buffer.concat([remoteNonce, invalidHmac]);
467
+
468
+ deviceV34['tmpLocalKey'] = localNonce;
469
+ deviceV34['socket'] = mockSocket;
470
+
471
+ const warnSpy = jest.spyOn(mockLogger, 'warn');
472
+ const disconnectSpy = jest.spyOn(deviceV34, 'disconnect');
473
+
474
+ deviceV34['_handleKeyExchangeResponse'](step2Payload);
475
+
476
+ // Should reject with warning and NOT disconnect (just returns)
477
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('HMAC mismatch'));
478
+ expect(disconnectSpy).not.toHaveBeenCalled();
479
+ });
480
+
481
+ test('rejects HMAC verification when key is wrong (critical bug scenario)', () => {
482
+ const { hmac } = require('../src/local/protocol/ProtocolUtilities');
483
+ const localNonce = Buffer.from('0123456789abcdef');
484
+ const remoteNonce = Buffer.from('fedcba9876543210');
485
+ const wrongKey = Buffer.from('wrongkeywrongkey');
486
+
487
+ // HMAC computed with wrong key
488
+ const hmacWithWrongKey = hmac(localNonce, wrongKey);
489
+ const step2Payload = Buffer.concat([remoteNonce, hmacWithWrongKey]);
490
+
491
+ deviceV34['tmpLocalKey'] = localNonce;
492
+ deviceV34['socket'] = mockSocket;
493
+
494
+ const warnSpy = jest.spyOn(mockLogger, 'warn');
495
+ const disconnectSpy = jest.spyOn(deviceV34, 'disconnect');
496
+ deviceV34['_handleKeyExchangeResponse'](step2Payload);
497
+
498
+ // Should reject because verification uses context.key, not the wrong key
499
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('HMAC mismatch'));
500
+ expect(disconnectSpy).not.toHaveBeenCalled();
501
+ });
502
+
503
+ test('accepts valid HMAC in key exchange step 2 (v3.5)', () => {
504
+ const { hmac } = require('../src/local/protocol/ProtocolUtilities');
505
+ const localNonce = Buffer.from('abcdef0123456789');
506
+ const remoteNonce = Buffer.from('9876543210fedcba');
507
+ const validHmac = hmac(localNonce, contextV35.key);
508
+ const step2Payload = Buffer.concat([remoteNonce, validHmac]);
509
+
510
+ deviceV35['tmpLocalKey'] = localNonce;
511
+ deviceV35['socket'] = mockSocket;
512
+
513
+ const connectSpy = jest.spyOn(deviceV35, 'emit');
514
+ deviceV35['_handleKeyExchangeResponse'](step2Payload);
515
+
516
+ expect(deviceV35.connected).toBe(true);
517
+ expect(connectSpy).toHaveBeenCalledWith('connect');
518
+ });
519
+ });
520
+
521
+ describe('session key derivation (v3.4 ECB vs v3.5 GCM)', () => {
522
+ let deviceV34: LocalDevice;
523
+ let deviceV35: LocalDevice;
524
+ let contextV34: LocalDeviceContext;
525
+ let contextV35: LocalDeviceContext;
526
+
527
+ beforeEach(() => {
528
+ contextV34 = {
529
+ id: 'device_v34',
530
+ key: Buffer.from('0123456789abcdef'),
531
+ ip: '192.168.1.100',
532
+ version: '3.4',
533
+ name: 'Test v3.4',
534
+ port: 6668,
535
+ };
536
+
537
+ contextV35 = {
538
+ id: 'device_v35',
539
+ key: Buffer.from('0123456789abcdef'),
540
+ ip: '192.168.1.101',
541
+ version: '3.5',
542
+ name: 'Test v3.5',
543
+ port: 6668,
544
+ };
545
+
546
+ deviceV34 = new LocalDevice(contextV34, mockLogger);
547
+ deviceV35 = new LocalDevice(contextV35, mockLogger);
548
+ });
549
+
550
+ afterEach(() => {
551
+ deviceV34.disconnect();
552
+ deviceV35.disconnect();
553
+ });
554
+
555
+ test('v3.4 derives session key using ECB encryption', () => {
556
+ const { hmac, encryptECBNoPad } = require('../src/local/protocol/ProtocolUtilities');
557
+ const localNonce = Buffer.from('0123456789abcdef');
558
+ const remoteNonce = Buffer.from('fedcba9876543210');
559
+ const validHmac = hmac(localNonce, contextV34.key);
560
+ const step2Payload = Buffer.concat([remoteNonce, validHmac]);
561
+
562
+ deviceV34['tmpLocalKey'] = localNonce;
563
+ deviceV34['socket'] = mockSocket;
564
+
565
+ deviceV34['_handleKeyExchangeResponse'](step2Payload);
566
+
567
+ // Verify session key was derived using ECB
568
+ const xored = Buffer.allocUnsafe(16);
569
+ for (let i = 0; i < 16; i++) xored[i] = localNonce[i] ^ remoteNonce[i];
570
+ const expectedSessionKey = encryptECBNoPad(xored, contextV34.key);
571
+ expect(deviceV34['sessionKey']).toEqual(expectedSessionKey);
572
+ });
573
+
574
+ test('v3.5 derives session key using GCM encryption', () => {
575
+ const { hmac, encryptGCM } = require('../src/local/protocol/ProtocolUtilities');
576
+ const localNonce = Buffer.from('abcdef0123456789');
577
+ const remoteNonce = Buffer.from('9876543210fedcba');
578
+ const validHmac = hmac(localNonce, contextV35.key);
579
+ const step2Payload = Buffer.concat([remoteNonce, validHmac]);
580
+
581
+ deviceV35['tmpLocalKey'] = localNonce;
582
+ deviceV35['socket'] = mockSocket;
583
+
584
+ deviceV35['_handleKeyExchangeResponse'](step2Payload);
585
+
586
+ // Verify session key was derived using GCM with IV=localNonce[:12]
587
+ const xored = Buffer.allocUnsafe(16);
588
+ for (let i = 0; i < 16; i++) xored[i] = localNonce[i] ^ remoteNonce[i];
589
+ const iv = localNonce.subarray(0, 12);
590
+ const { ciphertext } = encryptGCM(xored, contextV35.key, iv);
591
+ const expectedSessionKey = ciphertext.subarray(0, 16);
592
+ expect(deviceV35['sessionKey']).toEqual(expectedSessionKey);
593
+ });
594
+
595
+ test('v3.4 and v3.5 produce different session keys (different derivation)', () => {
596
+ const { hmac } = require('../src/local/protocol/ProtocolUtilities');
597
+ const localNonce = Buffer.from('0123456789abcdef');
598
+ const remoteNonce = Buffer.from('fedcba9876543210');
599
+ const validHmac = hmac(localNonce, contextV34.key);
600
+ const step2Payload = Buffer.concat([remoteNonce, validHmac]);
601
+
602
+ deviceV34['tmpLocalKey'] = localNonce;
603
+ deviceV34['socket'] = mockSocket;
604
+ deviceV35['tmpLocalKey'] = localNonce;
605
+ deviceV35['socket'] = mockSocket;
606
+
607
+ deviceV34['_handleKeyExchangeResponse'](step2Payload);
608
+ const validHmacV35 = hmac(localNonce, contextV35.key);
609
+ const step2PayloadV35 = Buffer.concat([remoteNonce, validHmacV35]);
610
+ deviceV35['_handleKeyExchangeResponse'](step2PayloadV35);
611
+
612
+ // Session keys should differ because derivation methods are different
613
+ expect(deviceV34['sessionKey']).not.toEqual(deviceV35['sessionKey']);
614
+ });
615
+ });
616
+ });