@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.
- package/CHANGELOG.md +19 -0
- package/README.md +161 -144
- package/config.schema.json +976 -11
- package/dist/{core → cloud/api}/TuyaOpenAPI.js +6 -7
- package/dist/cloud/api/TuyaOpenAPI.js.map +1 -0
- package/dist/{core → cloud/api}/TuyaOpenMQ.js +13 -11
- package/dist/cloud/api/TuyaOpenMQ.js.map +1 -0
- package/dist/cloud/device/TuyaCustomDeviceManager.js.map +1 -0
- package/dist/{device → cloud/device}/TuyaDevice.js +14 -1
- package/dist/cloud/device/TuyaDevice.js.map +1 -0
- package/dist/{device → cloud/device}/TuyaDeviceManager.js +8 -3
- package/dist/cloud/device/TuyaDeviceManager.js.map +1 -0
- package/dist/cloud/device/TuyaHomeDeviceManager.js.map +1 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/local/CloudLocalReconciliation.js +180 -0
- package/dist/local/CloudLocalReconciliation.js.map +1 -0
- package/dist/local/DynamicChildDiscovery.js +154 -0
- package/dist/local/DynamicChildDiscovery.js.map +1 -0
- package/dist/local/LocalDevice.js +419 -0
- package/dist/local/LocalDevice.js.map +1 -0
- package/dist/local/LocalDeviceManager.js +833 -0
- package/dist/local/LocalDeviceManager.js.map +1 -0
- package/dist/local/TuyaDiscovery.js +248 -0
- package/dist/local/TuyaDiscovery.js.map +1 -0
- package/dist/local/ZigbeeEventDetection.js +213 -0
- package/dist/local/ZigbeeEventDetection.js.map +1 -0
- package/dist/local/ZigbeeGatewayDetection.js +109 -0
- package/dist/local/ZigbeeGatewayDetection.js.map +1 -0
- package/dist/local/config.js +6 -0
- package/dist/local/config.js.map +1 -0
- package/dist/local/protocol/ChildPayloadUtility.js +94 -0
- package/dist/local/protocol/ChildPayloadUtility.js.map +1 -0
- package/dist/local/protocol/Protocol.js +3 -0
- package/dist/local/protocol/Protocol.js.map +1 -0
- package/dist/local/protocol/ProtocolFactory.js +27 -0
- package/dist/local/protocol/ProtocolFactory.js.map +1 -0
- package/dist/local/protocol/ProtocolUtilities.js +329 -0
- package/dist/local/protocol/ProtocolUtilities.js.map +1 -0
- package/dist/local/protocol/ProtocolV31V32.js +60 -0
- package/dist/local/protocol/ProtocolV31V32.js.map +1 -0
- package/dist/local/protocol/ProtocolV33.js +52 -0
- package/dist/local/protocol/ProtocolV33.js.map +1 -0
- package/dist/local/protocol/ProtocolV34.js +104 -0
- package/dist/local/protocol/ProtocolV34.js.map +1 -0
- package/dist/local/protocol/ProtocolV35.js +77 -0
- package/dist/local/protocol/ProtocolV35.js.map +1 -0
- package/dist/platform.js +149 -69
- package/dist/platform.js.map +1 -1
- package/dist/shared/AccessoryFactory.js +276 -0
- package/dist/shared/AccessoryFactory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/AccessoryFactory.js +2 -2
- package/dist/shared/accessories/AccessoryFactory.js.map +1 -0
- package/dist/shared/accessories/AirConditionerAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/AirPurifierAccessory.js +1 -1
- package/dist/shared/accessories/AirPurifierAccessory.js.map +1 -0
- package/dist/shared/accessories/AirQualitySensorAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/BaseAccessory.js +99 -30
- package/dist/shared/accessories/BaseAccessory.js.map +1 -0
- package/dist/shared/accessories/BlindsAccessory.js +199 -0
- package/dist/shared/accessories/BlindsAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/CameraAccessory.js +7 -3
- package/dist/shared/accessories/CameraAccessory.js.map +1 -0
- package/dist/shared/accessories/CarbonDioxideSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/CarbonMonoxideSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/ContactSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/DehumidifierAccessory.js.map +1 -0
- package/dist/shared/accessories/DiffuserAccessory.js.map +1 -0
- package/dist/shared/accessories/DimmerAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/DoorbellAccessory.js +3 -1
- package/dist/shared/accessories/DoorbellAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/ExtractionHoodAccessory.js +4 -2
- package/dist/shared/accessories/ExtractionHoodAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/FanAccessory.js +10 -4
- package/dist/shared/accessories/FanAccessory.js.map +1 -0
- package/dist/shared/accessories/GarageDoorAccessory.js.map +1 -0
- package/dist/shared/accessories/HeaterAccessory.js.map +1 -0
- package/dist/shared/accessories/HeaterAccessory_old.js.map +1 -0
- package/dist/shared/accessories/HumanPresenceSensorAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/HumidifierAccessory.js +3 -1
- package/dist/shared/accessories/HumidifierAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/IRAirConditionerAccessory.js +2 -2
- package/dist/shared/accessories/IRAirConditionerAccessory.js.map +1 -0
- package/dist/shared/accessories/IRControlHubAccessory.js.map +1 -0
- package/dist/shared/accessories/IRControlHubSubAccessory.js.map +1 -0
- package/dist/shared/accessories/IRGenericAccessory.js.map +1 -0
- package/dist/shared/accessories/LeakSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/LightAccessory.js.map +1 -0
- package/dist/shared/accessories/LightSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/LocationWeatherAccessory.js.map +1 -0
- package/dist/shared/accessories/LockAccessory.js.map +1 -0
- package/dist/shared/accessories/MotionSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/OutletAccessory.js.map +1 -0
- package/dist/shared/accessories/PetFeederAccessory.js.map +1 -0
- package/dist/shared/accessories/SceneAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/SceneSwitchAccessory.js +1 -1
- package/dist/shared/accessories/SceneSwitchAccessory.js.map +1 -0
- package/dist/shared/accessories/SecuritySystemAccessory.js.map +1 -0
- package/dist/shared/accessories/SmokeSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/SwitchAccessory.js +148 -0
- package/dist/shared/accessories/SwitchAccessory.js.map +1 -0
- package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js.map +1 -0
- package/dist/shared/accessories/ThermostatAccessory.js.map +1 -0
- package/dist/shared/accessories/TowerRackAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/ValveAccessory.js +1 -1
- package/dist/shared/accessories/ValveAccessory.js.map +1 -0
- package/dist/{accessory → shared/accessories}/VibrationSensorAccessory.js +7 -3
- package/dist/shared/accessories/VibrationSensorAccessory.js.map +1 -0
- package/dist/shared/accessories/WeatherStationAccessory.js.map +1 -0
- package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js.map +1 -0
- package/dist/shared/accessories/WhiteNoiseLightAccessory.js.map +1 -0
- package/dist/shared/accessories/WindowAccessory.js.map +1 -0
- package/dist/shared/accessories/WindowCoveringAccessory.js.map +1 -0
- package/dist/shared/accessories/WirelessSwitchAccessory.js.map +1 -0
- package/dist/shared/accessories/characteristic/Active.js.map +1 -0
- package/dist/{accessory → shared/accessories}/characteristic/AirQuality.js +1 -1
- package/dist/shared/accessories/characteristic/AirQuality.js.map +1 -0
- package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js.map +1 -0
- package/dist/shared/accessories/characteristic/CurrentTemperature.js.map +1 -0
- package/dist/shared/accessories/characteristic/CurrentWeather.js.map +1 -0
- package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js.map +1 -0
- package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js.map +1 -0
- package/dist/shared/accessories/characteristic/EnergyUsage.js.map +1 -0
- package/dist/shared/accessories/characteristic/Light.js.map +1 -0
- package/dist/shared/accessories/characteristic/LightSensor.js.map +1 -0
- package/dist/shared/accessories/characteristic/LockPhysicalControls.js.map +1 -0
- package/dist/{accessory → shared/accessories}/characteristic/MotionDetected.js +1 -1
- package/dist/shared/accessories/characteristic/MotionDetected.js.map +1 -0
- package/dist/shared/accessories/characteristic/Name.js.map +1 -0
- package/dist/shared/accessories/characteristic/OccupancyDetected.js.map +1 -0
- package/dist/{accessory → shared/accessories}/characteristic/On.js +1 -1
- package/dist/shared/accessories/characteristic/On.js.map +1 -0
- package/dist/shared/accessories/characteristic/OutletInUse.js.map +1 -0
- package/dist/{accessory → shared/accessories}/characteristic/ProgrammableSwitchEvent.js +2 -2
- package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js.map +1 -0
- package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js.map +1 -0
- package/dist/shared/accessories/characteristic/RotationSpeed.js.map +1 -0
- package/dist/shared/accessories/characteristic/SecuritySystemState.js.map +1 -0
- package/dist/shared/accessories/characteristic/SwingMode.js.map +1 -0
- package/dist/shared/accessories/characteristic/TargetTemperature.js.map +1 -0
- package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js.map +1 -0
- package/dist/shared/util/ConfigHash.js +79 -0
- package/dist/shared/util/ConfigHash.js.map +1 -0
- package/dist/shared/util/FfmpegStreamingProcess.js.map +1 -0
- package/dist/shared/util/InfraredTool.js.map +1 -0
- package/dist/shared/util/Logger.js.map +1 -0
- package/dist/shared/util/TuyaRecordingDelegate.js.map +1 -0
- package/dist/shared/util/TuyaStreamDelegate.js.map +1 -0
- package/dist/shared/util/color.js.map +1 -0
- package/dist/shared/util/util.js +135 -0
- package/dist/shared/util/util.js.map +1 -0
- package/eslint.config.mjs +52 -0
- package/jest.config.js +6 -0
- package/package.json +22 -27
- package/test/__mocks__/@homebridge-camera-utils.js +10 -0
- package/test/__mocks__/color-convert.js +24 -0
- package/test/__mocks__/kelvin-to-rgb.js +21 -0
- package/test/accessoryFactory.test.ts +522 -0
- package/test/config.test.ts +430 -0
- package/test/custom.test.ts +4 -4
- package/test/errorHandling.test.ts +570 -0
- package/test/errorHandlingCoverage.test.ts +410 -0
- package/test/home.test.ts +4 -4
- package/test/localDevice.test.ts +616 -0
- package/test/localDeviceManager.behavior.test.ts +185 -0
- package/test/localDeviceManager.test.ts +268 -0
- package/test/mqttMessages.test.ts +495 -0
- package/test/platform.test.ts +515 -0
- package/test/platform.validation.local.test.ts +250 -0
- package/test/protocol.test.ts +446 -0
- package/test/protocolUtilities.test.ts +351 -0
- package/test/sanitizeName.test.ts +2 -1
- package/test/tuyaCustomDeviceManager.test.ts +469 -0
- package/test/tuyaDevice.test.ts +425 -0
- package/test/tuyaDeviceManager.test.ts +813 -0
- package/test/tuyaDiscovery.test.ts +439 -0
- package/test/tuyaHomeDeviceManager.test.ts +540 -0
- package/test/tuyaOpenAPI.test.ts +404 -0
- package/test/tuyaOpenMQ.test.ts +444 -0
- package/test/util.test.ts +318 -0
- package/test/util.ts +4 -3
- package/test/zigbee/ChildPayloadUtility.test.ts +199 -0
- package/test/zigbee/CloudLocalReconciliation.test.ts +284 -0
- package/test/zigbee/DynamicChildDiscovery.test.ts +333 -0
- package/test/zigbee/LocalDevice.zigbee.test.ts +274 -0
- package/test/zigbee/LocalDeviceManager.cloud-discovery.test.ts +163 -0
- package/test/zigbee/LocalDeviceManager.zigbee.reconnect.test.ts +200 -0
- package/test/zigbee/LocalDeviceManager.zigbee.test.ts +344 -0
- package/test/zigbee/ZigbeeEventDetection.test.ts +418 -0
- package/test/zigbee/ZigbeeGatewayDetection.test.ts +222 -0
- package/tuya-local-configs/dev-1.hash +1 -0
- package/tuya-local-configs/device1.hash +1 -0
- package/wiki/Get-Local-Keys-for-Your-Devices.md +85 -0
- package/dist/accessory/AccessoryFactory.js.map +0 -1
- package/dist/accessory/AirConditionerAccessory.js.map +0 -1
- package/dist/accessory/AirPurifierAccessory.js.map +0 -1
- package/dist/accessory/AirQualitySensorAccessory.js.map +0 -1
- package/dist/accessory/BaseAccessory.js.map +0 -1
- package/dist/accessory/CameraAccessory.js.map +0 -1
- package/dist/accessory/CarbonDioxideSensorAccessory.js.map +0 -1
- package/dist/accessory/CarbonMonoxideSensorAccessory.js.map +0 -1
- package/dist/accessory/ContactSensorAccessory.js.map +0 -1
- package/dist/accessory/DehumidifierAccessory.js.map +0 -1
- package/dist/accessory/DiffuserAccessory.js.map +0 -1
- package/dist/accessory/DimmerAccessory.js.map +0 -1
- package/dist/accessory/DoorbellAccessory.js.map +0 -1
- package/dist/accessory/ExtractionHoodAccessory.js.map +0 -1
- package/dist/accessory/FanAccessory.js.map +0 -1
- package/dist/accessory/GarageDoorAccessory.js.map +0 -1
- package/dist/accessory/HeaterAccessory.js.map +0 -1
- package/dist/accessory/HeaterAccessory_old.js.map +0 -1
- package/dist/accessory/HumanPresenceSensorAccessory.js.map +0 -1
- package/dist/accessory/HumidifierAccessory.js.map +0 -1
- package/dist/accessory/IRAirConditionerAccessory.js.map +0 -1
- package/dist/accessory/IRControlHubAccessory.js.map +0 -1
- package/dist/accessory/IRControlHubSubAccessory.js.map +0 -1
- package/dist/accessory/IRGenericAccessory.js.map +0 -1
- package/dist/accessory/LeakSensorAccessory.js.map +0 -1
- package/dist/accessory/LightAccessory.js.map +0 -1
- package/dist/accessory/LightSensorAccessory.js.map +0 -1
- package/dist/accessory/LocationWeatherAccessory.js.map +0 -1
- package/dist/accessory/LockAccessory.js.map +0 -1
- package/dist/accessory/MotionSensorAccessory.js.map +0 -1
- package/dist/accessory/OutletAccessory.js.map +0 -1
- package/dist/accessory/PetFeederAccessory.js.map +0 -1
- package/dist/accessory/SceneAccessory.js.map +0 -1
- package/dist/accessory/SceneSwitchAccessory.js.map +0 -1
- package/dist/accessory/SecuritySystemAccessory.js.map +0 -1
- package/dist/accessory/SmokeSensorAccessory.js.map +0 -1
- package/dist/accessory/SwitchAccessory.js +0 -85
- package/dist/accessory/SwitchAccessory.js.map +0 -1
- package/dist/accessory/TemperatureHumiditySensorAccessory.js.map +0 -1
- package/dist/accessory/ThermostatAccessory.js.map +0 -1
- package/dist/accessory/TowerRackAccessory.js.map +0 -1
- package/dist/accessory/ValveAccessory.js.map +0 -1
- package/dist/accessory/VibrationSensorAccessory.js.map +0 -1
- package/dist/accessory/WeatherStationAccessory.js.map +0 -1
- package/dist/accessory/WetBulbGlobeTemperatureAccessory.js.map +0 -1
- package/dist/accessory/WhiteNoiseLightAccessory.js.map +0 -1
- package/dist/accessory/WindowAccessory.js.map +0 -1
- package/dist/accessory/WindowCoveringAccessory.js.map +0 -1
- package/dist/accessory/WirelessSwitchAccessory.js.map +0 -1
- package/dist/accessory/characteristic/Active.js.map +0 -1
- package/dist/accessory/characteristic/AirQuality.js.map +0 -1
- package/dist/accessory/characteristic/CurrentRelativeHumidity.js.map +0 -1
- package/dist/accessory/characteristic/CurrentTemperature.js.map +0 -1
- package/dist/accessory/characteristic/CurrentWeather.js.map +0 -1
- package/dist/accessory/characteristic/CurrentWeatherByOpenMeteo.js.map +0 -1
- package/dist/accessory/characteristic/CurrentWetBulbGlobeTemperature.js.map +0 -1
- package/dist/accessory/characteristic/EnergyUsage.js.map +0 -1
- package/dist/accessory/characteristic/Light.js.map +0 -1
- package/dist/accessory/characteristic/LightSensor.js.map +0 -1
- package/dist/accessory/characteristic/LockPhysicalControls.js.map +0 -1
- package/dist/accessory/characteristic/MotionDetected.js.map +0 -1
- package/dist/accessory/characteristic/Name.js.map +0 -1
- package/dist/accessory/characteristic/OccupancyDetected.js.map +0 -1
- package/dist/accessory/characteristic/On.js.map +0 -1
- package/dist/accessory/characteristic/OutletInUse.js.map +0 -1
- package/dist/accessory/characteristic/ProgrammableSwitchEvent.js.map +0 -1
- package/dist/accessory/characteristic/RelativeHumidityDehumidifierThreshold.js.map +0 -1
- package/dist/accessory/characteristic/RotationSpeed.js.map +0 -1
- package/dist/accessory/characteristic/SecuritySystemState.js.map +0 -1
- package/dist/accessory/characteristic/SwingMode.js.map +0 -1
- package/dist/accessory/characteristic/TargetTemperature.js.map +0 -1
- package/dist/accessory/characteristic/TemperatureDisplayUnits.js.map +0 -1
- package/dist/core/TuyaOpenAPI.js.map +0 -1
- package/dist/core/TuyaOpenMQ.js.map +0 -1
- package/dist/device/TuyaCustomDeviceManager.js.map +0 -1
- package/dist/device/TuyaDevice.js.map +0 -1
- package/dist/device/TuyaDeviceManager.js.map +0 -1
- package/dist/device/TuyaHomeDeviceManager.js.map +0 -1
- package/dist/util/FfmpegStreamingProcess.js.map +0 -1
- package/dist/util/InfraredTool.js.map +0 -1
- package/dist/util/Logger.js.map +0 -1
- package/dist/util/TuyaRecordingDelegate.js.map +0 -1
- package/dist/util/TuyaStreamDelegate.js.map +0 -1
- package/dist/util/color.js.map +0 -1
- package/dist/util/util.js +0 -63
- package/dist/util/util.js.map +0 -1
- /package/dist/{device → cloud/device}/TuyaCustomDeviceManager.js +0 -0
- /package/dist/{device → cloud/device}/TuyaHomeDeviceManager.js +0 -0
- /package/dist/{accessory → shared/accessories}/AirConditionerAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/AirQualitySensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/CarbonDioxideSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/CarbonMonoxideSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/ContactSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/DehumidifierAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/DiffuserAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/DimmerAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/GarageDoorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/HeaterAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/HeaterAccessory_old.js +0 -0
- /package/dist/{accessory → shared/accessories}/HumanPresenceSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/IRControlHubAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/IRControlHubSubAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/IRGenericAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/LeakSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/LightAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/LightSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/LocationWeatherAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/LockAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/MotionSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/OutletAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/PetFeederAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/SceneAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/SecuritySystemAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/SmokeSensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/TemperatureHumiditySensorAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/ThermostatAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/TowerRackAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/WeatherStationAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/WetBulbGlobeTemperatureAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/WhiteNoiseLightAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/WindowAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/WindowCoveringAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/WirelessSwitchAccessory.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/Active.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/CurrentRelativeHumidity.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/CurrentTemperature.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/CurrentWeather.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/CurrentWeatherByOpenMeteo.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/CurrentWetBulbGlobeTemperature.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/EnergyUsage.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/Light.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/LightSensor.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/LockPhysicalControls.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/Name.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/OccupancyDetected.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/OutletInUse.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/RelativeHumidityDehumidifierThreshold.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/RotationSpeed.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/SecuritySystemState.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/SwingMode.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/TargetTemperature.js +0 -0
- /package/dist/{accessory → shared/accessories}/characteristic/TemperatureDisplayUnits.js +0 -0
- /package/dist/{util → shared/util}/FfmpegStreamingProcess.js +0 -0
- /package/dist/{util → shared/util}/InfraredTool.js +0 -0
- /package/dist/{util → shared/util}/Logger.js +0 -0
- /package/dist/{util → shared/util}/TuyaRecordingDelegate.js +0 -0
- /package/dist/{util → shared/util}/TuyaStreamDelegate.js +0 -0
- /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
|
+
});
|