@homebridge-plugins/homebridge-tuya 3.0.0-beta.2 → 3.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +354 -142
- package/config.schema.json +1221 -893
- package/dist/cloud/api/TuyaOpenAPI.js +108 -60
- package/dist/cloud/api/TuyaOpenAPI.js.map +1 -1
- package/dist/cloud/api/TuyaOpenMQ.js +45 -22
- package/dist/cloud/api/TuyaOpenMQ.js.map +1 -1
- package/dist/cloud/device/{TuyaDeviceManager.js → TuyaCloudDeviceManager.js} +146 -96
- package/dist/cloud/device/TuyaCloudDeviceManager.js.map +1 -0
- package/dist/cloud/device/TuyaCustomDeviceManager.js +75 -5
- package/dist/cloud/device/TuyaCustomDeviceManager.js.map +1 -1
- package/dist/cloud/device/TuyaDevice.js.map +1 -1
- package/dist/cloud/device/TuyaHomeDeviceManager.js +81 -13
- package/dist/cloud/device/TuyaHomeDeviceManager.js.map +1 -1
- package/dist/config.js +26 -5
- package/dist/config.js.map +1 -1
- package/dist/local/DynamicChildDiscovery.js +2 -1
- package/dist/local/DynamicChildDiscovery.js.map +1 -1
- package/dist/local/LocalDevice.js +16 -3
- package/dist/local/LocalDevice.js.map +1 -1
- package/dist/local/LocalDeviceManager.js +361 -164
- package/dist/local/LocalDeviceManager.js.map +1 -1
- package/dist/local/TuyaDiscovery.js +16 -7
- package/dist/local/TuyaDiscovery.js.map +1 -1
- package/dist/local/ZigbeeGatewayDetection.js +7 -4
- package/dist/local/ZigbeeGatewayDetection.js.map +1 -1
- package/dist/local/protocol/ProtocolUtilities.js +34 -17
- package/dist/local/protocol/ProtocolUtilities.js.map +1 -1
- package/dist/local/protocol/ProtocolV31V32.js +4 -2
- package/dist/local/protocol/ProtocolV31V32.js.map +1 -1
- package/dist/local/protocol/ProtocolV33.js +4 -2
- package/dist/local/protocol/ProtocolV33.js.map +1 -1
- package/dist/local/protocol/ProtocolV34.js +10 -5
- package/dist/local/protocol/ProtocolV34.js.map +1 -1
- package/dist/local/protocol/ProtocolV35.js +8 -4
- package/dist/local/protocol/ProtocolV35.js.map +1 -1
- package/dist/platform.js +253 -493
- package/dist/platform.js.map +1 -1
- package/dist/settings.js +0 -2
- package/dist/settings.js.map +1 -1
- package/dist/shared/TuyaDeviceManager.js +147 -0
- package/dist/shared/TuyaDeviceManager.js.map +1 -0
- package/dist/shared/TuyaHybridDeviceManager.js +215 -0
- package/dist/shared/TuyaHybridDeviceManager.js.map +1 -0
- package/dist/shared/accessory/AccessoryFactory.js +350 -0
- package/dist/shared/accessory/AccessoryFactory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/AirConditionerAccessory.js +21 -15
- package/dist/shared/accessory/AirConditionerAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/AirPurifierAccessory.js.map +1 -1
- package/dist/shared/accessory/AirQualitySensorAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/BaseAccessory.js +16 -95
- package/dist/shared/accessory/BaseAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/BlindsAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/CameraAccessory.js +2 -2
- package/dist/shared/accessory/CameraAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/CarbonDioxideSensorAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/CarbonMonoxideSensorAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/ContactSensorAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/DehumidifierAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/DiffuserAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/DimmerAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/DoorbellAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/ExtractionHoodAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/FanAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/GarageDoorAccessory.js +19 -2
- package/dist/shared/accessory/GarageDoorAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/HeaterAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/HeaterAccessory_old.js.map +1 -1
- package/dist/shared/accessory/HumanPresenceSensorAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/HumidifierAccessory.js.map +1 -1
- package/dist/shared/accessory/IRAdapter.js +167 -0
- package/dist/shared/accessory/IRAdapter.js.map +1 -0
- package/dist/shared/accessory/IRAirConditionerAccessory.js.map +1 -0
- package/dist/shared/accessory/IRControlHubAccessory.js.map +1 -0
- package/dist/shared/accessory/IRControlHubSubAccessory.js.map +1 -0
- package/dist/shared/accessory/IRFanAccessory.js +90 -0
- package/dist/shared/accessory/IRFanAccessory.js.map +1 -0
- package/dist/shared/accessory/IRGenericAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/LeakSensorAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/LightAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/LightSensorAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/LocationWeatherAccessory.js +1 -1
- package/dist/shared/accessory/LocationWeatherAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/LockAccessory.js +3 -3
- package/dist/shared/{accessories → accessory}/LockAccessory.js.map +1 -1
- package/dist/shared/accessory/MotionSensorAccessory.js.map +1 -0
- package/dist/shared/accessory/OutletAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/PetFeederAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/SceneAccessory.js +1 -1
- package/dist/shared/accessory/SceneAccessory.js.map +1 -0
- package/dist/shared/accessory/SceneSwitchAccessory.js.map +1 -0
- package/dist/shared/accessory/SecuritySystemAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/SmokeSensorAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/SwitchAccessory.js +1 -45
- package/dist/shared/accessory/SwitchAccessory.js.map +1 -0
- package/dist/shared/accessory/TemperatureHumiditySensorAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/ThermostatAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/TowerRackAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/ValveAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/VibrationSensorAccessory.js.map +1 -1
- package/dist/shared/accessory/WeatherStationAccessory.js.map +1 -0
- package/dist/shared/accessory/WetBulbGlobeTemperatureAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/WhiteNoiseLightAccessory.js +1 -0
- package/dist/shared/accessory/WhiteNoiseLightAccessory.js.map +1 -0
- package/dist/shared/accessory/WindowAccessory.js.map +1 -0
- package/dist/shared/{accessories → accessory}/WindowCoveringAccessory.js.map +1 -1
- package/dist/shared/{accessories → accessory}/WirelessSwitchAccessory.js.map +1 -1
- package/dist/shared/accessory/characteristic/Active.js.map +1 -0
- package/dist/shared/accessory/characteristic/AirQuality.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/CurrentRelativeHumidity.js +3 -0
- package/dist/shared/accessory/characteristic/CurrentRelativeHumidity.js.map +1 -0
- package/dist/shared/accessory/characteristic/CurrentTemperature.js.map +1 -0
- package/dist/shared/accessory/characteristic/CurrentWeather.js.map +1 -0
- package/dist/shared/accessory/characteristic/CurrentWeatherByOpenMeteo.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/CurrentWetBulbGlobeTemperature.js +3 -2
- package/dist/shared/accessory/characteristic/CurrentWetBulbGlobeTemperature.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/EnergyUsage.js +0 -5
- package/dist/shared/accessory/characteristic/EnergyUsage.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/Light.js +28 -5
- package/dist/shared/accessory/characteristic/Light.js.map +1 -0
- package/dist/shared/accessory/characteristic/LightSensor.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/LockPhysicalControls.js +3 -0
- package/dist/shared/accessory/characteristic/LockPhysicalControls.js.map +1 -0
- package/dist/shared/accessory/characteristic/MotionDetected.js.map +1 -0
- package/dist/shared/accessory/characteristic/Name.js.map +1 -0
- package/dist/shared/accessory/characteristic/OccupancyDetected.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/On.js +1 -0
- package/dist/shared/accessory/characteristic/On.js.map +1 -0
- package/dist/shared/accessory/characteristic/OutletInUse.js.map +1 -0
- package/dist/shared/accessory/characteristic/ProgrammableSwitchEvent.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/RelativeHumidityDehumidifierThreshold.js +3 -0
- package/dist/shared/accessory/characteristic/RelativeHumidityDehumidifierThreshold.js.map +1 -0
- package/dist/shared/accessory/characteristic/RotationSpeed.js.map +1 -0
- package/dist/shared/accessory/characteristic/SecuritySystemState.js.map +1 -0
- package/dist/shared/{accessories → accessory}/characteristic/SwingMode.js +3 -0
- package/dist/shared/accessory/characteristic/SwingMode.js.map +1 -0
- package/dist/shared/accessory/characteristic/TargetTemperature.js.map +1 -0
- package/dist/shared/accessory/characteristic/TemperatureDisplayUnits.js.map +1 -0
- package/dist/shared/util/ConfigHash.js +2 -2
- package/dist/shared/util/ConfigHash.js.map +1 -1
- package/dist/shared/util/ConfigMigrator.js +222 -0
- package/dist/shared/util/ConfigMigrator.js.map +1 -0
- package/dist/shared/util/FfmpegStreamingProcess.js +62 -0
- package/dist/shared/util/FfmpegStreamingProcess.js.map +1 -1
- package/dist/shared/util/InfraredTool.js +0 -2
- package/dist/shared/util/InfraredTool.js.map +1 -1
- package/dist/shared/util/Logger.js +85 -10
- package/dist/shared/util/Logger.js.map +1 -1
- package/dist/shared/util/TuyaStreamDelegate.js +194 -97
- package/dist/shared/util/TuyaStreamDelegate.js.map +1 -1
- package/dist/shared/util/util.js +59 -4
- package/dist/shared/util/util.js.map +1 -1
- package/docs/ADVANCED_OPTIONS.md +603 -0
- package/docs/CONFIG_GUIDE.md +616 -0
- package/docs/Get-Local-Keys-for-Your-Devices.md +246 -0
- package/{SUPPORTED_DEVICES.md → docs/SUPPORTED_DEVICES.md} +24 -3
- package/eslint.config.mjs +0 -1
- package/{jest.config.js → jest.config.ts} +5 -2
- package/package.json +12 -9
- package/test/ConfigMigrator.test.ts +1060 -0
- package/test/Logger.test.ts +482 -0
- package/test/accessoryFactory.data.test.ts +172 -0
- package/test/accessoryFactory.test.ts +64 -35
- package/test/airConditioner.test.ts +742 -0
- package/test/config.test.ts +17 -13
- package/test/custom.test.ts +2 -2
- package/test/errorHandling.test.ts +12 -12
- package/test/home.test.ts +1 -1
- package/test/hybridDeviceManager.test.ts +808 -0
- package/test/light.test.ts +250 -0
- package/test/localDevice.test.ts +21 -50
- package/test/localDeviceManager.behavior.test.ts +196 -31
- package/test/localDeviceManager.test.ts +124 -41
- package/test/mqttMessages.test.ts +33 -14
- package/test/platform.test.ts +337 -814
- package/test/platform.validation.local.test.ts +58 -119
- package/test/tuyaCustomDeviceManager.test.ts +106 -16
- package/test/tuyaDeviceManager.test.ts +636 -639
- package/test/tuyaDiscovery.test.ts +21 -33
- package/test/tuyaHomeDeviceManager.test.ts +109 -18
- package/test/tuyaOpenAPI.test.ts +25 -44
- package/test/tuyaOpenMQ.test.ts +15 -23
- package/test/util.test.ts +107 -7
- package/test/zigbee/LocalDevice.zigbee.test.ts +14 -18
- package/test/zigbee/LocalDeviceManager.cloud-discovery.test.ts +19 -20
- package/test/zigbee/LocalDeviceManager.zigbee.reconnect.test.ts +19 -20
- package/test/zigbee/LocalDeviceManager.zigbee.test.ts +73 -35
- package/test/zigbee/ZigbeeGatewayDetection.test.ts +2 -2
- package/ADVANCED_OPTIONS.md +0 -267
- package/dist/cloud/device/TuyaDeviceManager.js.map +0 -1
- package/dist/local/CloudLocalReconciliation.js +0 -180
- package/dist/local/CloudLocalReconciliation.js.map +0 -1
- package/dist/shared/AccessoryFactory.js +0 -276
- package/dist/shared/AccessoryFactory.js.map +0 -1
- package/dist/shared/accessories/AccessoryFactory.js +0 -305
- package/dist/shared/accessories/AccessoryFactory.js.map +0 -1
- package/dist/shared/accessories/AirConditionerAccessory.js.map +0 -1
- package/dist/shared/accessories/AirQualitySensorAccessory.js.map +0 -1
- package/dist/shared/accessories/BaseAccessory.js.map +0 -1
- package/dist/shared/accessories/CameraAccessory.js.map +0 -1
- package/dist/shared/accessories/GarageDoorAccessory.js.map +0 -1
- package/dist/shared/accessories/HumanPresenceSensorAccessory.js.map +0 -1
- package/dist/shared/accessories/IRAirConditionerAccessory.js.map +0 -1
- package/dist/shared/accessories/IRControlHubAccessory.js.map +0 -1
- package/dist/shared/accessories/IRControlHubSubAccessory.js.map +0 -1
- package/dist/shared/accessories/IRGenericAccessory.js.map +0 -1
- package/dist/shared/accessories/LocationWeatherAccessory.js.map +0 -1
- package/dist/shared/accessories/MotionSensorAccessory.js.map +0 -1
- package/dist/shared/accessories/OutletAccessory.js.map +0 -1
- package/dist/shared/accessories/SceneAccessory.js.map +0 -1
- package/dist/shared/accessories/SceneSwitchAccessory.js.map +0 -1
- package/dist/shared/accessories/SecuritySystemAccessory.js.map +0 -1
- package/dist/shared/accessories/SwitchAccessory.js.map +0 -1
- package/dist/shared/accessories/TemperatureHumiditySensorAccessory.js.map +0 -1
- package/dist/shared/accessories/WeatherStationAccessory.js.map +0 -1
- package/dist/shared/accessories/WetBulbGlobeTemperatureAccessory.js.map +0 -1
- package/dist/shared/accessories/WhiteNoiseLightAccessory.js.map +0 -1
- package/dist/shared/accessories/WindowAccessory.js.map +0 -1
- package/dist/shared/accessories/characteristic/Active.js.map +0 -1
- package/dist/shared/accessories/characteristic/AirQuality.js.map +0 -1
- package/dist/shared/accessories/characteristic/CurrentRelativeHumidity.js.map +0 -1
- package/dist/shared/accessories/characteristic/CurrentTemperature.js.map +0 -1
- package/dist/shared/accessories/characteristic/CurrentWeather.js.map +0 -1
- package/dist/shared/accessories/characteristic/CurrentWeatherByOpenMeteo.js.map +0 -1
- package/dist/shared/accessories/characteristic/CurrentWetBulbGlobeTemperature.js.map +0 -1
- package/dist/shared/accessories/characteristic/EnergyUsage.js.map +0 -1
- package/dist/shared/accessories/characteristic/Light.js.map +0 -1
- package/dist/shared/accessories/characteristic/LightSensor.js.map +0 -1
- package/dist/shared/accessories/characteristic/LockPhysicalControls.js.map +0 -1
- package/dist/shared/accessories/characteristic/MotionDetected.js.map +0 -1
- package/dist/shared/accessories/characteristic/Name.js.map +0 -1
- package/dist/shared/accessories/characteristic/OccupancyDetected.js.map +0 -1
- package/dist/shared/accessories/characteristic/On.js.map +0 -1
- package/dist/shared/accessories/characteristic/OutletInUse.js.map +0 -1
- package/dist/shared/accessories/characteristic/ProgrammableSwitchEvent.js.map +0 -1
- package/dist/shared/accessories/characteristic/RelativeHumidityDehumidifierThreshold.js.map +0 -1
- package/dist/shared/accessories/characteristic/RotationSpeed.js.map +0 -1
- package/dist/shared/accessories/characteristic/SecuritySystemState.js.map +0 -1
- package/dist/shared/accessories/characteristic/SwingMode.js.map +0 -1
- package/dist/shared/accessories/characteristic/TargetTemperature.js.map +0 -1
- package/dist/shared/accessories/characteristic/TemperatureDisplayUnits.js.map +0 -1
- package/test/zigbee/CloudLocalReconciliation.test.ts +0 -284
- package/tuya-local-configs/dev-1.hash +0 -1
- package/tuya-local-configs/device1.hash +0 -1
- package/wiki/Get-Local-Keys-for-Your-Devices.md +0 -85
- /package/dist/shared/{accessories → accessory}/AirPurifierAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/AirQualitySensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/BlindsAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/CarbonDioxideSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/CarbonMonoxideSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/ContactSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/DehumidifierAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/DiffuserAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/DimmerAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/DoorbellAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/ExtractionHoodAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/FanAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/HeaterAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/HeaterAccessory_old.js +0 -0
- /package/dist/shared/{accessories → accessory}/HumanPresenceSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/HumidifierAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/IRAirConditionerAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/IRControlHubAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/IRControlHubSubAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/IRGenericAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/LeakSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/LightAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/LightSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/MotionSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/OutletAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/PetFeederAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/SceneSwitchAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/SecuritySystemAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/SmokeSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/TemperatureHumiditySensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/ThermostatAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/TowerRackAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/ValveAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/VibrationSensorAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/WeatherStationAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/WetBulbGlobeTemperatureAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/WindowAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/WindowCoveringAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/WirelessSwitchAccessory.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/Active.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/AirQuality.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/CurrentTemperature.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/CurrentWeather.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/CurrentWeatherByOpenMeteo.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/LightSensor.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/MotionDetected.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/Name.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/OccupancyDetected.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/OutletInUse.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/ProgrammableSwitchEvent.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/RotationSpeed.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/SecuritySystemState.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/TargetTemperature.js +0 -0
- /package/dist/shared/{accessories → accessory}/characteristic/TemperatureDisplayUnits.js +0 -0
package/test/platform.test.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { describe, expect, test, beforeEach, jest } from '@jest/globals';
|
|
3
3
|
import { TuyaPlatform } from '../src/platform';
|
|
4
|
+
import { PlatformAccessory } from 'homebridge';
|
|
5
|
+
import { ExLogger, initLogger } from '../src/shared/util/Logger';
|
|
6
|
+
import { TuyaPluginMode } from '../src/config';
|
|
7
|
+
import LocalDeviceManager from '../src/local/LocalDeviceManager';
|
|
8
|
+
import TuyaHybridDeviceManager from '../src/shared/TuyaHybridDeviceManager';
|
|
9
|
+
import TuyaHomeDeviceManager from '../src/cloud/device/TuyaHomeDeviceManager';
|
|
10
|
+
import TuyaCustomDeviceManager from '../src/cloud/device/TuyaCustomDeviceManager';
|
|
11
|
+
|
|
12
|
+
// Mock Logger
|
|
13
|
+
const mockLogger: ExLogger = {
|
|
14
|
+
debug: jest.fn(),
|
|
15
|
+
info: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
error: jest.fn(),
|
|
18
|
+
log: jest.fn(),
|
|
19
|
+
success: jest.fn(),
|
|
20
|
+
} as unknown as ExLogger;
|
|
4
21
|
|
|
5
22
|
// Mock Homebridge API
|
|
6
23
|
const mockHAP = {
|
|
@@ -11,6 +28,8 @@ const mockHAP = {
|
|
|
11
28
|
},
|
|
12
29
|
};
|
|
13
30
|
|
|
31
|
+
const errorStack:any[] = [];
|
|
32
|
+
|
|
14
33
|
const mockAPI = {
|
|
15
34
|
hap: mockHAP,
|
|
16
35
|
platformAccessory: jest.fn(),
|
|
@@ -19,20 +38,15 @@ const mockAPI = {
|
|
|
19
38
|
on: jest.fn(),
|
|
20
39
|
} as any;
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
PrefixLogger: class PrefixLogger {
|
|
32
|
-
constructor(public log: any, public name: string, public debug: boolean) {}
|
|
33
|
-
},
|
|
34
|
-
}));
|
|
35
|
-
|
|
41
|
+
jest.mock('jsonschema', () => {
|
|
42
|
+
return {
|
|
43
|
+
Validator: (
|
|
44
|
+
class MockValidator {
|
|
45
|
+
validate = jest.fn().mockReturnValue({ errors: errorStack });
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
};
|
|
49
|
+
});
|
|
36
50
|
// Mock file system
|
|
37
51
|
jest.mock('fs', () => ({
|
|
38
52
|
readFileSync: jest.fn(),
|
|
@@ -43,6 +57,10 @@ jest.mock('fs', () => ({
|
|
|
43
57
|
jest.mock('../src/cloud/api/TuyaOpenAPI', () => {
|
|
44
58
|
return class MockTuyaOpenAPI {
|
|
45
59
|
log = console;
|
|
60
|
+
|
|
61
|
+
static getDefaultEndpoint() {
|
|
62
|
+
return 'mock-endpoint';
|
|
63
|
+
}
|
|
46
64
|
};
|
|
47
65
|
});
|
|
48
66
|
|
|
@@ -50,34 +68,65 @@ jest.mock('../src/cloud/api/TuyaOpenMQ', () => {
|
|
|
50
68
|
return class MockTuyaOpenMQ {};
|
|
51
69
|
});
|
|
52
70
|
|
|
53
|
-
jest.mock('../src/cloud/device/
|
|
54
|
-
return class MockTuyaDeviceManager {
|
|
71
|
+
jest.mock('../src/cloud/device/TuyaCloudDeviceManager', () => {
|
|
72
|
+
return class MockTuyaDeviceManager {
|
|
73
|
+
pullDevices = jest.fn().mockReturnValue([]);
|
|
74
|
+
updateInfraredRemotes = jest.fn();
|
|
75
|
+
on = jest.fn();
|
|
76
|
+
devices = [];
|
|
77
|
+
};
|
|
55
78
|
});
|
|
56
79
|
|
|
57
80
|
jest.mock('../src/cloud/device/TuyaCustomDeviceManager', () => {
|
|
58
|
-
return class MockTuyaCustomDeviceManager {
|
|
81
|
+
return class MockTuyaCustomDeviceManager {
|
|
82
|
+
pullDevices = jest.fn().mockReturnValue([]);
|
|
83
|
+
updateInfraredRemotes = jest.fn();
|
|
84
|
+
on = jest.fn();
|
|
85
|
+
devices = [];
|
|
86
|
+
};
|
|
59
87
|
});
|
|
60
88
|
|
|
61
89
|
jest.mock('../src/cloud/device/TuyaHomeDeviceManager', () => {
|
|
62
|
-
return class MockTuyaHomeDeviceManager {
|
|
90
|
+
return class MockTuyaHomeDeviceManager {
|
|
91
|
+
pullDevices = jest.fn().mockReturnValue([]);
|
|
92
|
+
updateInfraredRemotes = jest.fn();
|
|
93
|
+
on = jest.fn();
|
|
94
|
+
devices = [];
|
|
95
|
+
};
|
|
63
96
|
});
|
|
64
97
|
|
|
65
98
|
// Mock Local components
|
|
66
99
|
jest.mock('../src/local/LocalDeviceManager', () => {
|
|
67
|
-
return class MockLocalDeviceManager {
|
|
100
|
+
return class MockLocalDeviceManager {
|
|
101
|
+
pullDevices = jest.fn().mockReturnValue([]);
|
|
102
|
+
updateInfraredRemotes = jest.fn();
|
|
103
|
+
on = jest.fn();
|
|
104
|
+
devices = [];
|
|
105
|
+
static isTargetDevice:any = jest.fn(s => true);
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
jest.mock('../src/shared/TuyaHybridDeviceManager', () => {
|
|
110
|
+
return class MockTuyaHybridDeviceManager {
|
|
111
|
+
pullDevices = jest.fn().mockReturnValue([]);
|
|
112
|
+
updateInfraredRemotes = jest.fn();
|
|
113
|
+
on = jest.fn();
|
|
114
|
+
devices = [];
|
|
115
|
+
};
|
|
68
116
|
});
|
|
69
117
|
|
|
70
118
|
// Mock Accessories
|
|
71
|
-
jest.mock('../src/shared/
|
|
119
|
+
jest.mock('../src/shared/accessory/AccessoryFactory', () => {
|
|
72
120
|
return {
|
|
73
121
|
default: class MockAccessoryFactory {},
|
|
74
122
|
};
|
|
75
123
|
});
|
|
76
124
|
|
|
77
|
-
jest.mock('../src/shared/util/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
125
|
+
jest.mock('../src/shared/util/ConfigMigrator', () => {
|
|
126
|
+
return class ConfigMigrator {
|
|
127
|
+
migrate(arg:any) { return arg; }
|
|
128
|
+
}
|
|
129
|
+
});
|
|
81
130
|
|
|
82
131
|
describe('TuyaPlatform', () => {
|
|
83
132
|
let platform: TuyaPlatform;
|
|
@@ -85,18 +134,22 @@ describe('TuyaPlatform', () => {
|
|
|
85
134
|
let mockLog: any;
|
|
86
135
|
|
|
87
136
|
beforeEach(() => {
|
|
137
|
+
jest.clearAllMocks();
|
|
138
|
+
initLogger(mockLogger);
|
|
139
|
+
|
|
88
140
|
mockLog = {
|
|
89
141
|
debug: jest.fn(),
|
|
90
142
|
info: jest.fn(),
|
|
91
143
|
warn: jest.fn(),
|
|
92
144
|
error: jest.fn(),
|
|
145
|
+
success: jest.fn(),
|
|
93
146
|
};
|
|
94
147
|
|
|
95
148
|
mockConfig = {
|
|
96
149
|
platform: 'TuyaPlatform',
|
|
97
150
|
name: 'Tuya',
|
|
98
151
|
mode: 'cloud',
|
|
99
|
-
|
|
152
|
+
cloud: {
|
|
100
153
|
projectType: '2',
|
|
101
154
|
accessId: 'test_id',
|
|
102
155
|
accessKey: 'test_key',
|
|
@@ -109,6 +162,8 @@ describe('TuyaPlatform', () => {
|
|
|
109
162
|
},
|
|
110
163
|
};
|
|
111
164
|
|
|
165
|
+
process.removeAllListeners('uncaughtException');
|
|
166
|
+
process.removeAllListeners('unhandledRejection');
|
|
112
167
|
platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
|
|
113
168
|
});
|
|
114
169
|
|
|
@@ -136,8 +191,8 @@ describe('TuyaPlatform', () => {
|
|
|
136
191
|
});
|
|
137
192
|
|
|
138
193
|
test('initializes cached accessories array', () => {
|
|
139
|
-
expect(platform
|
|
140
|
-
expect(
|
|
194
|
+
expect(platform['platformAccessories']).toEqual(new Map());
|
|
195
|
+
expect(platform['platformAccessories'] instanceof Map).toBe(true);
|
|
141
196
|
});
|
|
142
197
|
|
|
143
198
|
test('initializes accessory handlers array', () => {
|
|
@@ -155,7 +210,7 @@ describe('TuyaPlatform', () => {
|
|
|
155
210
|
const localConfig = {
|
|
156
211
|
...mockConfig,
|
|
157
212
|
mode: 'local',
|
|
158
|
-
|
|
213
|
+
cloud: undefined,
|
|
159
214
|
local: {
|
|
160
215
|
devices: [],
|
|
161
216
|
},
|
|
@@ -180,16 +235,16 @@ describe('TuyaPlatform', () => {
|
|
|
180
235
|
});
|
|
181
236
|
|
|
182
237
|
describe('configuration validation', () => {
|
|
183
|
-
test('requires
|
|
184
|
-
const
|
|
238
|
+
test('requires cloud for cloud mode', () => {
|
|
239
|
+
const noCloudConfig = {
|
|
185
240
|
platform: 'TuyaPlatform',
|
|
186
241
|
name: 'Tuya',
|
|
187
242
|
mode: 'cloud',
|
|
188
|
-
|
|
243
|
+
cloud: undefined,
|
|
189
244
|
};
|
|
190
245
|
|
|
191
|
-
const
|
|
192
|
-
const isValid =
|
|
246
|
+
const noCloudPlatform = new TuyaPlatform(mockLog, noCloudConfig, mockAPI);
|
|
247
|
+
const isValid = noCloudPlatform.validate();
|
|
193
248
|
|
|
194
249
|
expect(isValid).toBe(false);
|
|
195
250
|
});
|
|
@@ -213,7 +268,7 @@ describe('TuyaPlatform', () => {
|
|
|
213
268
|
platform: 'TuyaPlatform',
|
|
214
269
|
name: 'Tuya',
|
|
215
270
|
mode: 'cloud',
|
|
216
|
-
|
|
271
|
+
cloud: {
|
|
217
272
|
projectType: '1',
|
|
218
273
|
endpoint: 'https://openapi.tuyaeu.com',
|
|
219
274
|
accessId: 'id',
|
|
@@ -226,7 +281,7 @@ describe('TuyaPlatform', () => {
|
|
|
226
281
|
};
|
|
227
282
|
|
|
228
283
|
const cloudPlatform = new TuyaPlatform(mockLog, cloudConfig, mockAPI);
|
|
229
|
-
expect(cloudPlatform.
|
|
284
|
+
expect(cloudPlatform.platformConfig!.cloud!.projectType).toBe('1');
|
|
230
285
|
});
|
|
231
286
|
|
|
232
287
|
test('validates cloud config with projectType 2', () => {
|
|
@@ -234,7 +289,7 @@ describe('TuyaPlatform', () => {
|
|
|
234
289
|
platform: 'TuyaPlatform',
|
|
235
290
|
name: 'Tuya',
|
|
236
291
|
mode: 'cloud',
|
|
237
|
-
|
|
292
|
+
cloud: {
|
|
238
293
|
projectType: '2',
|
|
239
294
|
accessId: 'id',
|
|
240
295
|
accessKey: 'key',
|
|
@@ -248,7 +303,7 @@ describe('TuyaPlatform', () => {
|
|
|
248
303
|
};
|
|
249
304
|
|
|
250
305
|
const cloudPlatform = new TuyaPlatform(mockLog, cloudConfig, mockAPI);
|
|
251
|
-
expect(cloudPlatform.
|
|
306
|
+
expect(cloudPlatform.platformConfig!.cloud!.projectType).toBe('2');
|
|
252
307
|
});
|
|
253
308
|
});
|
|
254
309
|
|
|
@@ -295,13 +350,13 @@ describe('TuyaPlatform', () => {
|
|
|
295
350
|
});
|
|
296
351
|
|
|
297
352
|
test('stores cloud options', () => {
|
|
298
|
-
expect(platform.
|
|
299
|
-
expect(platform.
|
|
353
|
+
expect(platform.platformConfig.cloud).toBeDefined();
|
|
354
|
+
expect(platform.platformConfig!.cloud!.projectType).toBe('2');
|
|
300
355
|
});
|
|
301
356
|
|
|
302
357
|
test('accesses credentials from config', () => {
|
|
303
|
-
expect(platform.
|
|
304
|
-
expect(platform.
|
|
358
|
+
expect(platform.platformConfig!.cloud!.accessId).toBe('test_id');
|
|
359
|
+
expect(platform.platformConfig!.cloud!.accessKey).toBe('test_key');
|
|
305
360
|
});
|
|
306
361
|
});
|
|
307
362
|
|
|
@@ -329,38 +384,62 @@ describe('TuyaPlatform', () => {
|
|
|
329
384
|
|
|
330
385
|
test('caches accessories', () => {
|
|
331
386
|
const mockAccessory = { UUID: 'uuid-1', displayName: 'Cached Device' };
|
|
332
|
-
platform.
|
|
387
|
+
platform.configureAccessory(mockAccessory as PlatformAccessory);
|
|
333
388
|
|
|
334
|
-
expect(platform.
|
|
389
|
+
expect(platform['platformAccessories'].size).toBe(1);
|
|
335
390
|
});
|
|
336
391
|
|
|
337
392
|
test('can restore cached accessories', () => {
|
|
338
393
|
const mockAccessory1 = { UUID: 'uuid-1', displayName: 'Device 1' };
|
|
339
394
|
const mockAccessory2 = { UUID: 'uuid-2', displayName: 'Device 2' };
|
|
395
|
+
[mockAccessory1, mockAccessory2].forEach(mockAccessory => platform.configureAccessory(mockAccessory as PlatformAccessory));
|
|
340
396
|
|
|
341
|
-
platform
|
|
342
|
-
|
|
343
|
-
expect(platform.cachedAccessories.length).toBe(2);
|
|
397
|
+
expect(platform['platformAccessories'].size).toBe(2);
|
|
344
398
|
});
|
|
345
399
|
});
|
|
346
400
|
|
|
347
401
|
describe('device manager initialization', () => {
|
|
348
|
-
test('can initialize
|
|
402
|
+
test('can initialize device manager', () => {
|
|
349
403
|
platform.deviceManager = {} as any;
|
|
350
404
|
expect(platform.deviceManager).toBeDefined();
|
|
351
405
|
});
|
|
352
406
|
|
|
353
|
-
test('
|
|
354
|
-
|
|
355
|
-
|
|
407
|
+
test('initialize custom device manager', () => {
|
|
408
|
+
mockConfig.mode = TuyaPluginMode.cloud;
|
|
409
|
+
mockConfig.cloud.projectType = '1';
|
|
410
|
+
const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
|
|
411
|
+
platform.initDevices().then(() => {
|
|
412
|
+
expect(platform.mode).toBe(mockConfig.mode);
|
|
413
|
+
expect(platform.deviceManager).toBeInstanceOf(TuyaCustomDeviceManager);
|
|
414
|
+
});
|
|
356
415
|
});
|
|
357
416
|
|
|
358
|
-
test('
|
|
359
|
-
|
|
360
|
-
|
|
417
|
+
test('initialize home device manager', () => {
|
|
418
|
+
mockConfig.mode = TuyaPluginMode.cloud;
|
|
419
|
+
mockConfig.cloud.projectType = '2';
|
|
420
|
+
const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
|
|
421
|
+
platform.initDevices().then(() => {
|
|
422
|
+
expect(platform.mode).toBe(mockConfig.mode);
|
|
423
|
+
expect(platform.deviceManager).toBeInstanceOf(TuyaHomeDeviceManager);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
361
426
|
|
|
362
|
-
|
|
363
|
-
|
|
427
|
+
test('initialize local device manager', () => {
|
|
428
|
+
mockConfig.mode = TuyaPluginMode.local;
|
|
429
|
+
const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
|
|
430
|
+
platform.initDevices().then(() => {
|
|
431
|
+
expect(platform.mode).toBe(mockConfig.mode);
|
|
432
|
+
expect(platform.deviceManager).toBeInstanceOf(LocalDeviceManager);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('initialize both device manager', () => {
|
|
437
|
+
mockConfig.mode = TuyaPluginMode.both;
|
|
438
|
+
const platform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
|
|
439
|
+
platform.initDevices().then(() => {
|
|
440
|
+
expect(platform.mode).toBe(mockConfig.mode);
|
|
441
|
+
expect(platform.deviceManager).toBeInstanceOf(TuyaHybridDeviceManager);
|
|
442
|
+
});
|
|
364
443
|
});
|
|
365
444
|
});
|
|
366
445
|
|
|
@@ -370,7 +449,7 @@ describe('TuyaPlatform', () => {
|
|
|
370
449
|
platform: 'TuyaPlatform',
|
|
371
450
|
name: 'Tuya',
|
|
372
451
|
mode: 'both',
|
|
373
|
-
|
|
452
|
+
cloud: {
|
|
374
453
|
projectType: '2',
|
|
375
454
|
accessId: 'id',
|
|
376
455
|
accessKey: 'key',
|
|
@@ -388,183 +467,8 @@ describe('TuyaPlatform', () => {
|
|
|
388
467
|
|
|
389
468
|
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
390
469
|
expect(bothPlatform.mode).toBe('both');
|
|
391
|
-
expect(bothPlatform.
|
|
392
|
-
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test('enriches local config with cloud device details', async () => {
|
|
396
|
-
const bothConfig = {
|
|
397
|
-
platform: 'TuyaPlatform',
|
|
398
|
-
name: 'Tuya',
|
|
399
|
-
mode: 'both',
|
|
400
|
-
options: {
|
|
401
|
-
projectType: '2',
|
|
402
|
-
accessId: 'id',
|
|
403
|
-
accessKey: 'key',
|
|
404
|
-
countryCode: 1,
|
|
405
|
-
username: 'user',
|
|
406
|
-
password: 'pass',
|
|
407
|
-
appSchema: 'tuyaSmart',
|
|
408
|
-
generateWeatherAccessory: false,
|
|
409
|
-
weatherAPI: '',
|
|
410
|
-
},
|
|
411
|
-
local: {
|
|
412
|
-
devices: [],
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
417
|
-
|
|
418
|
-
// Mock cloud device
|
|
419
|
-
const mockCloudDevice = {
|
|
420
|
-
id: 'test-device-123',
|
|
421
|
-
uuid: 'test-device-123',
|
|
422
|
-
name: 'Test Device',
|
|
423
|
-
product_id: 'product-123',
|
|
424
|
-
} as any;
|
|
425
|
-
|
|
426
|
-
// Mock the getDeviceDetails response
|
|
427
|
-
const mockDeviceDetails = {
|
|
428
|
-
success: true,
|
|
429
|
-
result: {
|
|
430
|
-
id: 'test-device-123',
|
|
431
|
-
local_key: 'test-local-key-abcdef',
|
|
432
|
-
ip: '192.168.1.100',
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
bothPlatform.deviceManager = {
|
|
437
|
-
getDeviceDetails: async () => mockDeviceDetails,
|
|
438
|
-
} as any;
|
|
439
|
-
|
|
440
|
-
// Enrich local config
|
|
441
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
442
|
-
|
|
443
|
-
// Verify local config was enriched
|
|
444
|
-
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
445
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
446
|
-
expect(bothPlatform.platformConfig.local.devices.length).toBeGreaterThan(0);
|
|
447
|
-
|
|
448
|
-
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
449
|
-
expect(enrichedDevice.tuyaDeviceId).toBe('test-device-123');
|
|
450
|
-
expect(enrichedDevice.tuyaKey).toBe('test-local-key-abcdef');
|
|
451
|
-
expect(enrichedDevice.ip).toBe('192.168.1.100');
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test('preserves manual local config when enriching', async () => {
|
|
456
|
-
const bothConfig = {
|
|
457
|
-
platform: 'TuyaPlatform',
|
|
458
|
-
name: 'Tuya',
|
|
459
|
-
mode: 'both',
|
|
460
|
-
options: {
|
|
461
|
-
projectType: '2',
|
|
462
|
-
accessId: 'id',
|
|
463
|
-
accessKey: 'key',
|
|
464
|
-
countryCode: 1,
|
|
465
|
-
username: 'user',
|
|
466
|
-
password: 'pass',
|
|
467
|
-
appSchema: 'tuyaSmart',
|
|
468
|
-
generateWeatherAccessory: false,
|
|
469
|
-
weatherAPI: '',
|
|
470
|
-
},
|
|
471
|
-
local: {
|
|
472
|
-
devices: [
|
|
473
|
-
{
|
|
474
|
-
tuyaDeviceId: 'manual-device-456',
|
|
475
|
-
tuyaKey: 'manual-key-xyz',
|
|
476
|
-
name: 'Manual Device',
|
|
477
|
-
},
|
|
478
|
-
],
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
483
|
-
|
|
484
|
-
// Mock cloud device (different from manual one)
|
|
485
|
-
const mockCloudDevice = {
|
|
486
|
-
id: 'test-device-123',
|
|
487
|
-
uuid: 'test-device-123',
|
|
488
|
-
name: 'Test Device',
|
|
489
|
-
product_id: 'product-123',
|
|
490
|
-
} as any;
|
|
491
|
-
|
|
492
|
-
bothPlatform.deviceManager = {
|
|
493
|
-
getDeviceDetails: async () => ({
|
|
494
|
-
success: true,
|
|
495
|
-
result: {
|
|
496
|
-
id: 'test-device-123',
|
|
497
|
-
local_key: 'cloud-key-123',
|
|
498
|
-
ip: '192.168.1.100',
|
|
499
|
-
},
|
|
500
|
-
}),
|
|
501
|
-
} as any;
|
|
502
|
-
|
|
503
|
-
// Enrich local config
|
|
504
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
505
|
-
|
|
506
|
-
// Verify local config exists and has devices
|
|
507
|
-
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
508
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
509
|
-
// Verify manual device is still there
|
|
510
|
-
const manualDevice = bothPlatform.platformConfig.local.devices.find(
|
|
511
|
-
d => d.tuyaDeviceId === 'manual-device-456'
|
|
512
|
-
);
|
|
513
|
-
expect(manualDevice).toBeDefined();
|
|
514
|
-
expect(manualDevice?.tuyaKey).toBe('manual-key-xyz');
|
|
515
|
-
|
|
516
|
-
// Verify new cloud device was added
|
|
517
|
-
const cloudDevice = bothPlatform.platformConfig.local.devices.find(
|
|
518
|
-
d => d.tuyaDeviceId === 'test-device-123'
|
|
519
|
-
);
|
|
520
|
-
expect(cloudDevice).toBeDefined();
|
|
521
|
-
expect(cloudDevice?.tuyaKey).toBe('cloud-key-123');
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
test('skips enrichment if cloud device details unavailable', async () => {
|
|
526
|
-
const bothConfig = {
|
|
527
|
-
platform: 'TuyaPlatform',
|
|
528
|
-
name: 'Tuya',
|
|
529
|
-
mode: 'both',
|
|
530
|
-
options: {
|
|
531
|
-
projectType: '2',
|
|
532
|
-
accessId: 'id',
|
|
533
|
-
accessKey: 'key',
|
|
534
|
-
countryCode: 1,
|
|
535
|
-
username: 'user',
|
|
536
|
-
password: 'pass',
|
|
537
|
-
appSchema: 'tuyaSmart',
|
|
538
|
-
generateWeatherAccessory: false,
|
|
539
|
-
weatherAPI: '',
|
|
540
|
-
},
|
|
541
|
-
local: {
|
|
542
|
-
devices: [],
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
547
|
-
|
|
548
|
-
const mockCloudDevice = {
|
|
549
|
-
id: 'test-device-123',
|
|
550
|
-
name: 'Test Device',
|
|
551
|
-
} as any;
|
|
552
|
-
|
|
553
|
-
bothPlatform.deviceManager = {
|
|
554
|
-
getDeviceDetails: async () => ({
|
|
555
|
-
success: false,
|
|
556
|
-
code: 'NOT_FOUND',
|
|
557
|
-
}),
|
|
558
|
-
} as any;
|
|
559
|
-
|
|
560
|
-
// Enrich local config
|
|
561
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
562
|
-
|
|
563
|
-
// Verify nothing was added (since API failed)
|
|
470
|
+
expect(bothPlatform.platformConfig.cloud).toBeDefined();
|
|
564
471
|
expect(bothPlatform.platformConfig.local).toBeDefined();
|
|
565
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
566
|
-
expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
|
|
567
|
-
}
|
|
568
472
|
});
|
|
569
473
|
});
|
|
570
474
|
|
|
@@ -624,13 +528,135 @@ describe('TuyaPlatform', () => {
|
|
|
624
528
|
});
|
|
625
529
|
});
|
|
626
530
|
|
|
531
|
+
describe('validate', () => {
|
|
532
|
+
test('local mode without local config', () => {
|
|
533
|
+
const invalidConfig = {
|
|
534
|
+
platform: 'TuyaPlatform',
|
|
535
|
+
name: 'Tuya',
|
|
536
|
+
mode: 'local',
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const spyError = jest.spyOn(mockLog, 'error');
|
|
540
|
+
|
|
541
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
542
|
+
const isValid = invalidPlatform.validate();
|
|
543
|
+
|
|
544
|
+
expect(isValid).toBe(false);
|
|
545
|
+
expect(spyError).toHaveBeenCalledWith('mode is "local" but no "local" config block found.');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test('both mode without local config', () => {
|
|
549
|
+
const invalidConfig = {
|
|
550
|
+
platform: 'TuyaPlatform',
|
|
551
|
+
name: 'Tuya',
|
|
552
|
+
mode: 'both',
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const spyError = jest.spyOn(mockLog, 'error');
|
|
556
|
+
|
|
557
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
558
|
+
const isValid = invalidPlatform.validate();
|
|
559
|
+
|
|
560
|
+
expect(isValid).toBe(false);
|
|
561
|
+
expect(spyError).toHaveBeenCalledWith('mode is "both" but no "local" config block found.');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('cloud mode without cloud config', () => {
|
|
565
|
+
const invalidConfig = {
|
|
566
|
+
platform: 'TuyaPlatform',
|
|
567
|
+
name: 'Tuya',
|
|
568
|
+
mode: 'cloud',
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const spyError = jest.spyOn(mockLog, 'error');
|
|
572
|
+
|
|
573
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
574
|
+
const isValid = invalidPlatform.validate();
|
|
575
|
+
|
|
576
|
+
expect(isValid).toBe(false);
|
|
577
|
+
expect(spyError).toHaveBeenCalledWith('Not configured — "cloud" block is required for cloud mode, exit.');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('both mode without cloud config', () => {
|
|
581
|
+
const invalidConfig = {
|
|
582
|
+
platform: 'TuyaPlatform',
|
|
583
|
+
name: 'Tuya',
|
|
584
|
+
mode: 'both',
|
|
585
|
+
local: {}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const spyError = jest.spyOn(mockLog, 'error');
|
|
589
|
+
|
|
590
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
591
|
+
const isValid = invalidPlatform.validate();
|
|
592
|
+
|
|
593
|
+
expect(isValid).toBe(false);
|
|
594
|
+
expect(spyError).toHaveBeenCalledWith('Not configured — "cloud" block is required for cloud mode, exit.');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test('Validator().validate returns error', () => {
|
|
598
|
+
const invalidConfig = {
|
|
599
|
+
platform: 'TuyaPlatform',
|
|
600
|
+
name: 'Tuya',
|
|
601
|
+
mode: 'cloud',
|
|
602
|
+
cloud: { projectType: '1', deviceOverides: [] }
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const stack = { stack: 'error stack' };
|
|
606
|
+
errorStack.push(stack);
|
|
607
|
+
|
|
608
|
+
const spyError = jest.spyOn(mockLog, 'error');
|
|
609
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
610
|
+
|
|
611
|
+
expect(spyError).toHaveBeenCalledWith(stack.stack);
|
|
612
|
+
errorStack.splice(0, errorStack.length);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('cloud mode with invalid deviceOverrides', () => {
|
|
616
|
+
const invalidConfig = {
|
|
617
|
+
platform: 'TuyaPlatform',
|
|
618
|
+
name: 'Tuya',
|
|
619
|
+
mode: 'cloud',
|
|
620
|
+
cloud: { projectType: '1' }
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
624
|
+
invalidPlatform.validateDeviceOverrides = jest.fn(() => false);
|
|
625
|
+
const spyMethod = jest.spyOn(invalidPlatform, 'validateDeviceOverrides');
|
|
626
|
+
const isValid = invalidPlatform.validate();
|
|
627
|
+
|
|
628
|
+
expect(spyMethod).toHaveBeenCalledTimes(1);
|
|
629
|
+
expect(isValid).toBe(false);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test('cloud mode with invalid deviceOverride.schema', () => {
|
|
633
|
+
const invalidConfig = {
|
|
634
|
+
platform: 'TuyaPlatform',
|
|
635
|
+
name: 'Tuya',
|
|
636
|
+
mode: 'cloud',
|
|
637
|
+
cloud: { projectType: '1' }
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const invalidPlatform = new TuyaPlatform(mockLog, invalidConfig, mockAPI);
|
|
641
|
+
invalidPlatform.validateDeviceOverrides = jest.fn(() => true);
|
|
642
|
+
invalidPlatform.validateSchema = jest.fn(() => false);
|
|
643
|
+
const spyMethod1 = jest.spyOn(invalidPlatform, 'validateDeviceOverrides');
|
|
644
|
+
const spyMethod2 = jest.spyOn(invalidPlatform, 'validateSchema');
|
|
645
|
+
const isValid = invalidPlatform.validate();
|
|
646
|
+
|
|
647
|
+
expect(spyMethod1).toHaveBeenCalledTimes(1);
|
|
648
|
+
expect(spyMethod2).toHaveBeenCalledTimes(1);
|
|
649
|
+
expect(isValid).toBe(false);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
627
653
|
describe('configuration edge cases', () => {
|
|
628
654
|
test('handles missing project type', () => {
|
|
629
655
|
const noProjectTypeConfig = {
|
|
630
656
|
platform: 'TuyaPlatform',
|
|
631
657
|
name: 'Tuya',
|
|
632
658
|
mode: 'cloud',
|
|
633
|
-
|
|
659
|
+
cloud: {
|
|
634
660
|
accessId: 'id',
|
|
635
661
|
accessKey: 'key',
|
|
636
662
|
},
|
|
@@ -657,15 +683,15 @@ describe('TuyaPlatform', () => {
|
|
|
657
683
|
test('handles weather accessory option', () => {
|
|
658
684
|
const weatherConfig = {
|
|
659
685
|
...mockConfig,
|
|
660
|
-
|
|
661
|
-
...mockConfig.
|
|
686
|
+
cloud: {
|
|
687
|
+
...mockConfig.cloud,
|
|
662
688
|
generateWeatherAccessory: true,
|
|
663
689
|
weatherAPI: 'openweathermap',
|
|
664
690
|
},
|
|
665
691
|
};
|
|
666
692
|
|
|
667
693
|
const weatherPlatform = new TuyaPlatform(mockLog, weatherConfig, mockAPI);
|
|
668
|
-
expect((weatherPlatform.
|
|
694
|
+
expect((weatherPlatform.platformConfig.cloud as any).generateWeatherAccessory).toBe(true);
|
|
669
695
|
});
|
|
670
696
|
});
|
|
671
697
|
|
|
@@ -684,20 +710,21 @@ describe('TuyaPlatform', () => {
|
|
|
684
710
|
const platform1 = new TuyaPlatform(mockLog, config1, mockAPI);
|
|
685
711
|
const platform2 = new TuyaPlatform(mockLog, config2, mockAPI);
|
|
686
712
|
|
|
687
|
-
platform1.
|
|
713
|
+
platform1.configureAccessory({} as PlatformAccessory);
|
|
714
|
+
|
|
688
715
|
|
|
689
|
-
expect(platform1.
|
|
690
|
-
expect(platform2.
|
|
716
|
+
expect(platform1['platformAccessories'].size).toBe(1);
|
|
717
|
+
expect(platform2['platformAccessories'].size).toBe(0);
|
|
691
718
|
});
|
|
692
719
|
});
|
|
693
720
|
|
|
694
|
-
describe('
|
|
695
|
-
test('
|
|
721
|
+
describe('device ID consistency and deduplication', () => {
|
|
722
|
+
test('uses consistent UUID for same device across cloud and local', () => {
|
|
696
723
|
const bothConfig = {
|
|
697
724
|
platform: 'TuyaPlatform',
|
|
698
725
|
name: 'Tuya',
|
|
699
726
|
mode: 'both',
|
|
700
|
-
|
|
727
|
+
cloud: {
|
|
701
728
|
projectType: '2',
|
|
702
729
|
accessId: 'id',
|
|
703
730
|
accessKey: 'key',
|
|
@@ -715,595 +742,91 @@ describe('TuyaPlatform', () => {
|
|
|
715
742
|
|
|
716
743
|
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
717
744
|
|
|
745
|
+
// Create mock cloud and local devices with same ID
|
|
718
746
|
const mockCloudDevice = {
|
|
719
|
-
id: '
|
|
720
|
-
uuid: '
|
|
721
|
-
name: 'Test Device',
|
|
722
|
-
product_id: 'product-123',
|
|
747
|
+
id: 'same-device-id',
|
|
748
|
+
uuid: 'same-device-id',
|
|
749
|
+
name: 'Test Device (Cloud)',
|
|
723
750
|
} as any;
|
|
724
751
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
result: {
|
|
730
|
-
id: 'test-device-123',
|
|
731
|
-
localKey: 'alternate-format-key', // camelCase variant
|
|
732
|
-
ip: '192.168.1.100',
|
|
733
|
-
},
|
|
734
|
-
}),
|
|
752
|
+
const mockLocalDevice = {
|
|
753
|
+
id: 'same-device-id',
|
|
754
|
+
uuid: 'same-device-id',
|
|
755
|
+
name: 'Test Device (Local)',
|
|
735
756
|
} as any;
|
|
736
757
|
|
|
737
|
-
|
|
758
|
+
// Both should map to same UUID
|
|
759
|
+
const cloudUUID = bothPlatform.api.hap.uuid.generate(mockCloudDevice.id);
|
|
760
|
+
const localUUID = bothPlatform.api.hap.uuid.generate(mockLocalDevice.id);
|
|
738
761
|
|
|
739
|
-
|
|
740
|
-
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
741
|
-
expect(enrichedDevice.tuyaKey).toBe('alternate-format-key');
|
|
742
|
-
}
|
|
762
|
+
expect(cloudUUID).toBe(localUUID);
|
|
743
763
|
});
|
|
764
|
+
});
|
|
744
765
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
mode: 'both',
|
|
750
|
-
options: {
|
|
751
|
-
projectType: '2',
|
|
752
|
-
accessId: 'id',
|
|
753
|
-
accessKey: 'key',
|
|
754
|
-
countryCode: 1,
|
|
755
|
-
username: 'user',
|
|
756
|
-
password: 'pass',
|
|
757
|
-
appSchema: 'tuyaSmart',
|
|
758
|
-
generateWeatherAccessory: false,
|
|
759
|
-
weatherAPI: '',
|
|
760
|
-
},
|
|
761
|
-
local: {
|
|
762
|
-
devices: [],
|
|
763
|
-
},
|
|
766
|
+
describe('syncDeviceAndAccessory', () => {
|
|
767
|
+
test('device config changed', () => {
|
|
768
|
+
const mockDevice = {
|
|
769
|
+
id: 'mock-id'
|
|
764
770
|
};
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
result: {
|
|
777
|
-
id: 'test-device-456',
|
|
778
|
-
local_key: 'test-key-456',
|
|
779
|
-
address: '10.0.0.50', // Uses 'address' instead of 'ip'
|
|
780
|
-
},
|
|
781
|
-
}),
|
|
782
|
-
} as any;
|
|
783
|
-
|
|
784
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
785
|
-
|
|
786
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
787
|
-
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
788
|
-
expect(enrichedDevice.ip).toBe('10.0.0.50');
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
test('uses uuid fallback when cloud device id is missing', async () => {
|
|
793
|
-
const bothConfig = {
|
|
794
|
-
platform: 'TuyaPlatform',
|
|
795
|
-
name: 'Tuya',
|
|
796
|
-
mode: 'both',
|
|
797
|
-
options: {
|
|
798
|
-
projectType: '2',
|
|
799
|
-
accessId: 'id',
|
|
800
|
-
accessKey: 'key',
|
|
801
|
-
countryCode: 1,
|
|
802
|
-
username: 'user',
|
|
803
|
-
password: 'pass',
|
|
804
|
-
appSchema: 'tuyaSmart',
|
|
805
|
-
generateWeatherAccessory: false,
|
|
806
|
-
weatherAPI: '',
|
|
807
|
-
},
|
|
808
|
-
local: {
|
|
809
|
-
devices: [],
|
|
810
|
-
},
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
814
|
-
|
|
815
|
-
const mockCloudDevice = {
|
|
816
|
-
uuid: 'fallback-device-123',
|
|
817
|
-
name: 'Fallback Device',
|
|
818
|
-
} as any;
|
|
819
|
-
|
|
820
|
-
bothPlatform.deviceManager = {
|
|
821
|
-
getDeviceDetails: async () => ({
|
|
822
|
-
success: true,
|
|
823
|
-
result: {
|
|
824
|
-
uuid: 'fallback-device-123',
|
|
825
|
-
local_key: 'fallback-key-123',
|
|
826
|
-
ip: '192.168.1.101',
|
|
771
|
+
const mockOldAccessory = {
|
|
772
|
+
context: { deviceConfigHash: 'hash2' },
|
|
773
|
+
services: [
|
|
774
|
+
{
|
|
775
|
+
UUID: 'service-uuid1',
|
|
776
|
+
characteristics: [
|
|
777
|
+
{ UUID: 'characteristic-uuid1' },
|
|
778
|
+
{ UUID: 'characteristic-uuid2' },
|
|
779
|
+
{ UUID: 'characteristic-uuid3' },
|
|
780
|
+
],
|
|
781
|
+
removeCharacteristic: jest.fn()
|
|
827
782
|
},
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
test('propagates cloud category into local config', async () => {
|
|
841
|
-
const bothConfig = {
|
|
842
|
-
platform: 'TuyaPlatform',
|
|
843
|
-
name: 'Tuya',
|
|
844
|
-
mode: 'both',
|
|
845
|
-
options: {
|
|
846
|
-
projectType: '2',
|
|
847
|
-
accessId: 'id',
|
|
848
|
-
accessKey: 'key',
|
|
849
|
-
countryCode: 1,
|
|
850
|
-
username: 'user',
|
|
851
|
-
password: 'pass',
|
|
852
|
-
appSchema: 'tuyaSmart',
|
|
853
|
-
generateWeatherAccessory: false,
|
|
854
|
-
weatherAPI: '',
|
|
855
|
-
},
|
|
856
|
-
local: {
|
|
857
|
-
devices: [],
|
|
858
|
-
},
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
862
|
-
|
|
863
|
-
const mockCloudDevice = {
|
|
864
|
-
id: 'category-device-123',
|
|
865
|
-
name: 'Category Device',
|
|
866
|
-
category: 'kg',
|
|
867
|
-
product_id: 'abc123',
|
|
868
|
-
} as any;
|
|
869
|
-
|
|
870
|
-
bothPlatform.deviceManager = {
|
|
871
|
-
getDeviceDetails: async () => ({
|
|
872
|
-
success: true,
|
|
873
|
-
result: {
|
|
874
|
-
id: 'category-device-123',
|
|
875
|
-
local_key: 'category-key-123',
|
|
876
|
-
ip: '192.168.1.102',
|
|
877
|
-
},
|
|
878
|
-
}),
|
|
879
|
-
} as any;
|
|
880
|
-
|
|
881
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
882
|
-
|
|
883
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
884
|
-
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
885
|
-
expect(enrichedDevice.category).toBe('kg');
|
|
886
|
-
}
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
test('handles missing local_key gracefully', async () => {
|
|
890
|
-
const bothConfig = {
|
|
891
|
-
platform: 'TuyaPlatform',
|
|
892
|
-
name: 'Tuya',
|
|
893
|
-
mode: 'both',
|
|
894
|
-
options: {
|
|
895
|
-
projectType: '2',
|
|
896
|
-
accessId: 'id',
|
|
897
|
-
accessKey: 'key',
|
|
898
|
-
countryCode: 1,
|
|
899
|
-
username: 'user',
|
|
900
|
-
password: 'pass',
|
|
901
|
-
appSchema: 'tuyaSmart',
|
|
902
|
-
generateWeatherAccessory: false,
|
|
903
|
-
weatherAPI: '',
|
|
904
|
-
},
|
|
905
|
-
local: {
|
|
906
|
-
devices: [],
|
|
907
|
-
},
|
|
908
|
-
};
|
|
909
|
-
|
|
910
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
911
|
-
|
|
912
|
-
const mockCloudDevice = {
|
|
913
|
-
id: 'test-device-no-key',
|
|
914
|
-
name: 'Device Without Local Key',
|
|
915
|
-
} as any;
|
|
916
|
-
|
|
917
|
-
bothPlatform.deviceManager = {
|
|
918
|
-
getDeviceDetails: async () => ({
|
|
919
|
-
success: true,
|
|
920
|
-
result: {
|
|
921
|
-
id: 'test-device-no-key',
|
|
922
|
-
// No local_key or localKey provided
|
|
923
|
-
ip: '192.168.1.200',
|
|
924
|
-
},
|
|
925
|
-
}),
|
|
926
|
-
} as any;
|
|
927
|
-
|
|
928
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
929
|
-
|
|
930
|
-
// Device should not be added to local config without local_key
|
|
931
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
932
|
-
expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
test('handles API timeout with grace', async () => {
|
|
937
|
-
const bothConfig = {
|
|
938
|
-
platform: 'TuyaPlatform',
|
|
939
|
-
name: 'Tuya',
|
|
940
|
-
mode: 'both',
|
|
941
|
-
options: {
|
|
942
|
-
projectType: '2',
|
|
943
|
-
accessId: 'id',
|
|
944
|
-
accessKey: 'key',
|
|
945
|
-
countryCode: 1,
|
|
946
|
-
username: 'user',
|
|
947
|
-
password: 'pass',
|
|
948
|
-
appSchema: 'tuyaSmart',
|
|
949
|
-
generateWeatherAccessory: false,
|
|
950
|
-
weatherAPI: '',
|
|
951
|
-
},
|
|
952
|
-
local: {
|
|
953
|
-
devices: [],
|
|
954
|
-
},
|
|
955
|
-
};
|
|
956
|
-
|
|
957
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
958
|
-
|
|
959
|
-
const mockCloudDevice = {
|
|
960
|
-
id: 'test-device-timeout',
|
|
961
|
-
name: 'Timeout Device',
|
|
962
|
-
} as any;
|
|
963
|
-
|
|
964
|
-
bothPlatform.deviceManager = {
|
|
965
|
-
getDeviceDetails: async () => {
|
|
966
|
-
throw new Error('Request timeout');
|
|
967
|
-
},
|
|
968
|
-
} as any;
|
|
969
|
-
|
|
970
|
-
// Should not throw, but handle gracefully
|
|
971
|
-
await expect(bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice])).resolves.toBeUndefined();
|
|
972
|
-
|
|
973
|
-
// Local devices should remain empty (timeout treated as error)
|
|
974
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
975
|
-
expect(bothPlatform.platformConfig.local.devices.length).toBe(0);
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
describe('mixed config scenarios', () => {
|
|
981
|
-
test('updates manual local config with cloud-provided local_key', async () => {
|
|
982
|
-
const bothConfig = {
|
|
983
|
-
platform: 'TuyaPlatform',
|
|
984
|
-
name: 'Tuya',
|
|
985
|
-
mode: 'both',
|
|
986
|
-
options: {
|
|
987
|
-
projectType: '2',
|
|
988
|
-
accessId: 'id',
|
|
989
|
-
accessKey: 'key',
|
|
990
|
-
countryCode: 1,
|
|
991
|
-
username: 'user',
|
|
992
|
-
password: 'pass',
|
|
993
|
-
appSchema: 'tuyaSmart',
|
|
994
|
-
generateWeatherAccessory: false,
|
|
995
|
-
weatherAPI: '',
|
|
996
|
-
},
|
|
997
|
-
local: {
|
|
998
|
-
devices: [
|
|
999
|
-
{
|
|
1000
|
-
tuyaDeviceId: 'manual-device-xyz',
|
|
1001
|
-
// User provided device ID but no key
|
|
1002
|
-
name: 'Manual Device No Key',
|
|
1003
|
-
},
|
|
1004
|
-
],
|
|
1005
|
-
},
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1009
|
-
|
|
1010
|
-
const mockCloudDevice = {
|
|
1011
|
-
id: 'manual-device-xyz',
|
|
1012
|
-
name: 'Manual Device No Key',
|
|
1013
|
-
} as any;
|
|
1014
|
-
|
|
1015
|
-
bothPlatform.deviceManager = {
|
|
1016
|
-
getDeviceDetails: async () => ({
|
|
1017
|
-
success: true,
|
|
1018
|
-
result: {
|
|
1019
|
-
id: 'manual-device-xyz',
|
|
1020
|
-
local_key: 'cloud-filled-key',
|
|
1021
|
-
ip: '192.168.1.99',
|
|
1022
|
-
},
|
|
1023
|
-
}),
|
|
1024
|
-
} as any;
|
|
1025
|
-
|
|
1026
|
-
await bothPlatform.enrichLocalConfigFromCloud([mockCloudDevice]);
|
|
1027
|
-
|
|
1028
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
1029
|
-
const enrichedDevice = bothPlatform.platformConfig.local.devices[0];
|
|
1030
|
-
expect(enrichedDevice.tuyaDeviceId).toBe('manual-device-xyz');
|
|
1031
|
-
expect(enrichedDevice.tuyaKey).toBe('cloud-filled-key');
|
|
1032
|
-
expect(enrichedDevice.ip).toBe('192.168.1.99');
|
|
1033
|
-
// Original name should be preserved
|
|
1034
|
-
expect(enrichedDevice.name).toBe('Manual Device No Key');
|
|
1035
|
-
}
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
test('does not overwrite existing manual local_key', async () => {
|
|
1039
|
-
const bothConfig = {
|
|
1040
|
-
platform: 'TuyaPlatform',
|
|
1041
|
-
name: 'Tuya',
|
|
1042
|
-
mode: 'both',
|
|
1043
|
-
options: {
|
|
1044
|
-
projectType: '2',
|
|
1045
|
-
accessId: 'id',
|
|
1046
|
-
accessKey: 'key',
|
|
1047
|
-
countryCode: 1,
|
|
1048
|
-
username: 'user',
|
|
1049
|
-
password: 'pass',
|
|
1050
|
-
appSchema: 'tuyaSmart',
|
|
1051
|
-
generateWeatherAccessory: false,
|
|
1052
|
-
weatherAPI: '',
|
|
1053
|
-
},
|
|
1054
|
-
local: {
|
|
1055
|
-
devices: [
|
|
1056
|
-
{
|
|
1057
|
-
tuyaDeviceId: 'device-with-manual-key',
|
|
1058
|
-
tuyaKey: 'user-provided-key-123', // User explicitly set this
|
|
1059
|
-
name: 'Device With Manual Key',
|
|
1060
|
-
},
|
|
1061
|
-
],
|
|
1062
|
-
},
|
|
783
|
+
{
|
|
784
|
+
UUID: 'service-uuid2',
|
|
785
|
+
characteristics: [
|
|
786
|
+
{ UUID: 'characteristic-uuid1' },
|
|
787
|
+
{ UUID: 'characteristic-uuid3' },
|
|
788
|
+
{ UUID: 'characteristic-uuid4' },
|
|
789
|
+
]
|
|
790
|
+
}
|
|
791
|
+
],
|
|
792
|
+
removeService: jest.fn()
|
|
1063
793
|
};
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
getDeviceDetails: async () => ({
|
|
1074
|
-
success: true,
|
|
1075
|
-
result: {
|
|
1076
|
-
id: 'device-with-manual-key',
|
|
1077
|
-
local_key: 'different-cloud-key',
|
|
1078
|
-
ip: '192.168.1.88',
|
|
794
|
+
const mockNewAccessory = {
|
|
795
|
+
services: [
|
|
796
|
+
{
|
|
797
|
+
UUID: 'service-uuid1',
|
|
798
|
+
characteristics: [
|
|
799
|
+
{ UUID: 'characteristic-uuid1' },
|
|
800
|
+
{ UUID: 'characteristic-uuid3' },
|
|
801
|
+
{ UUID: 'characteristic-uuid5' },
|
|
802
|
+
]
|
|
1079
803
|
},
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
// Manual key should be skipped (device has tuyaKey already)
|
|
1088
|
-
expect(enrichedDevice.tuyaKey).toBe('user-provided-key-123');
|
|
1089
|
-
}
|
|
1090
|
-
});
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
describe('multi-device enrichment', () => {
|
|
1094
|
-
test('enriches multiple cloud devices in parallel', async () => {
|
|
1095
|
-
const bothConfig = {
|
|
1096
|
-
platform: 'TuyaPlatform',
|
|
1097
|
-
name: 'Tuya',
|
|
1098
|
-
mode: 'both',
|
|
1099
|
-
options: {
|
|
1100
|
-
projectType: '2',
|
|
1101
|
-
accessId: 'id',
|
|
1102
|
-
accessKey: 'key',
|
|
1103
|
-
countryCode: 1,
|
|
1104
|
-
username: 'user',
|
|
1105
|
-
password: 'pass',
|
|
1106
|
-
appSchema: 'tuyaSmart',
|
|
1107
|
-
generateWeatherAccessory: false,
|
|
1108
|
-
weatherAPI: '',
|
|
1109
|
-
},
|
|
1110
|
-
local: {
|
|
1111
|
-
devices: [],
|
|
1112
|
-
},
|
|
1113
|
-
};
|
|
1114
|
-
|
|
1115
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1116
|
-
|
|
1117
|
-
const mockCloudDevices = [
|
|
1118
|
-
{ id: 'device-1', uuid: 'device-1', name: 'Device 1', product_id: 'prod-1' },
|
|
1119
|
-
{ id: 'device-2', uuid: 'device-2', name: 'Device 2', product_id: 'prod-2' },
|
|
1120
|
-
{ id: 'device-3', uuid: 'device-3', name: 'Device 3', product_id: 'prod-3' },
|
|
1121
|
-
] as any;
|
|
1122
|
-
|
|
1123
|
-
const callCount: Record<string, number> = {};
|
|
1124
|
-
bothPlatform.deviceManager = {
|
|
1125
|
-
getDeviceDetails: async (deviceId: string) => {
|
|
1126
|
-
callCount[deviceId] = (callCount[deviceId] || 0) + 1;
|
|
1127
|
-
return {
|
|
1128
|
-
success: true,
|
|
1129
|
-
result: {
|
|
1130
|
-
id: deviceId,
|
|
1131
|
-
local_key: `key-${deviceId}`,
|
|
1132
|
-
ip: `192.168.1.${Math.floor(Math.random() * 254) + 1}`,
|
|
1133
|
-
},
|
|
1134
|
-
};
|
|
1135
|
-
},
|
|
1136
|
-
} as any;
|
|
1137
|
-
|
|
1138
|
-
await bothPlatform.enrichLocalConfigFromCloud(mockCloudDevices);
|
|
1139
|
-
|
|
1140
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
1141
|
-
expect(bothPlatform.platformConfig.local.devices.length).toBe(3);
|
|
1142
|
-
|
|
1143
|
-
const device1 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-1');
|
|
1144
|
-
const device2 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-2');
|
|
1145
|
-
const device3 = bothPlatform.platformConfig.local.devices.find(d => d.tuyaDeviceId === 'device-3');
|
|
1146
|
-
|
|
1147
|
-
expect(device1?.tuyaKey).toBe('key-device-1');
|
|
1148
|
-
expect(device2?.tuyaKey).toBe('key-device-2');
|
|
1149
|
-
expect(device3?.tuyaKey).toBe('key-device-3');
|
|
1150
|
-
}
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
test('continues enrichment if single device fails', async () => {
|
|
1154
|
-
const bothConfig = {
|
|
1155
|
-
platform: 'TuyaPlatform',
|
|
1156
|
-
name: 'Tuya',
|
|
1157
|
-
mode: 'both',
|
|
1158
|
-
options: {
|
|
1159
|
-
projectType: '2',
|
|
1160
|
-
accessId: 'id',
|
|
1161
|
-
accessKey: 'key',
|
|
1162
|
-
countryCode: 1,
|
|
1163
|
-
username: 'user',
|
|
1164
|
-
password: 'pass',
|
|
1165
|
-
appSchema: 'tuyaSmart',
|
|
1166
|
-
generateWeatherAccessory: false,
|
|
1167
|
-
weatherAPI: '',
|
|
1168
|
-
},
|
|
1169
|
-
local: {
|
|
1170
|
-
devices: [],
|
|
1171
|
-
},
|
|
1172
|
-
};
|
|
1173
|
-
|
|
1174
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1175
|
-
|
|
1176
|
-
const mockCloudDevices = [
|
|
1177
|
-
{ id: 'device-ok-1', uuid: 'device-ok-1', name: 'OK Device 1' },
|
|
1178
|
-
{ id: 'device-fail', uuid: 'device-fail', name: 'Failing Device' },
|
|
1179
|
-
{ id: 'device-ok-2', uuid: 'device-ok-2', name: 'OK Device 2' },
|
|
1180
|
-
] as any;
|
|
1181
|
-
|
|
1182
|
-
bothPlatform.deviceManager = {
|
|
1183
|
-
getDeviceDetails: async (deviceId: string) => {
|
|
1184
|
-
if (deviceId === 'device-fail') {
|
|
1185
|
-
return { success: false, code: 'ERROR' };
|
|
804
|
+
{
|
|
805
|
+
UUID: 'service-uuid3',
|
|
806
|
+
characteristics: [
|
|
807
|
+
{ UUID: 'characteristic-uuid2' },
|
|
808
|
+
{ UUID: 'characteristic-uuid3' },
|
|
809
|
+
{ UUID: 'characteristic-uuid4' },
|
|
810
|
+
]
|
|
1186
811
|
}
|
|
1187
|
-
|
|
1188
|
-
success: true,
|
|
1189
|
-
result: {
|
|
1190
|
-
id: deviceId,
|
|
1191
|
-
local_key: `key-${deviceId}`,
|
|
1192
|
-
ip: '192.168.1.100',
|
|
1193
|
-
},
|
|
1194
|
-
};
|
|
1195
|
-
},
|
|
1196
|
-
} as any;
|
|
1197
|
-
|
|
1198
|
-
await bothPlatform.enrichLocalConfigFromCloud(mockCloudDevices);
|
|
1199
|
-
|
|
1200
|
-
if (bothPlatform.platformConfig.local?.devices) {
|
|
1201
|
-
// Should have 2 devices (one failed, skipped)
|
|
1202
|
-
expect(bothPlatform.platformConfig.local.devices.length).toBe(2);
|
|
1203
|
-
|
|
1204
|
-
const hasOk1 = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-ok-1');
|
|
1205
|
-
const hasOk2 = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-ok-2');
|
|
1206
|
-
const hasFailed = bothPlatform.platformConfig.local.devices.some(d => d.tuyaDeviceId === 'device-fail');
|
|
1207
|
-
|
|
1208
|
-
expect(hasOk1).toBe(true);
|
|
1209
|
-
expect(hasOk2).toBe(true);
|
|
1210
|
-
expect(hasFailed).toBe(false);
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
describe('device ID consistency and deduplication', () => {
|
|
1216
|
-
test('uses consistent UUID for same device across cloud and local', () => {
|
|
1217
|
-
const bothConfig = {
|
|
1218
|
-
platform: 'TuyaPlatform',
|
|
1219
|
-
name: 'Tuya',
|
|
1220
|
-
mode: 'both',
|
|
1221
|
-
options: {
|
|
1222
|
-
projectType: '2',
|
|
1223
|
-
accessId: 'id',
|
|
1224
|
-
accessKey: 'key',
|
|
1225
|
-
countryCode: 1,
|
|
1226
|
-
username: 'user',
|
|
1227
|
-
password: 'pass',
|
|
1228
|
-
appSchema: 'tuyaSmart',
|
|
1229
|
-
generateWeatherAccessory: false,
|
|
1230
|
-
weatherAPI: '',
|
|
1231
|
-
},
|
|
1232
|
-
local: {
|
|
1233
|
-
devices: [],
|
|
1234
|
-
},
|
|
1235
|
-
};
|
|
1236
|
-
|
|
1237
|
-
const bothPlatform = new TuyaPlatform(mockLog, bothConfig, mockAPI);
|
|
1238
|
-
|
|
1239
|
-
// Create mock cloud and local devices with same ID
|
|
1240
|
-
const mockCloudDevice = {
|
|
1241
|
-
id: 'same-device-id',
|
|
1242
|
-
uuid: 'same-device-id',
|
|
1243
|
-
name: 'Test Device (Cloud)',
|
|
1244
|
-
} as any;
|
|
1245
|
-
|
|
1246
|
-
const mockLocalDevice = {
|
|
1247
|
-
id: 'same-device-id',
|
|
1248
|
-
uuid: 'same-device-id',
|
|
1249
|
-
name: 'Test Device (Local)',
|
|
1250
|
-
} as any;
|
|
1251
|
-
|
|
1252
|
-
// Both should map to same UUID
|
|
1253
|
-
const cloudUUID = bothPlatform.api.hap.uuid.generate(mockCloudDevice.id);
|
|
1254
|
-
const localUUID = bothPlatform.api.hap.uuid.generate(mockLocalDevice.id);
|
|
1255
|
-
|
|
1256
|
-
expect(cloudUUID).toBe(localUUID);
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
test('enrichment preserves device ID for accessory mapping', async () => {
|
|
1260
|
-
const bothConfig = {
|
|
1261
|
-
platform: 'TuyaPlatform',
|
|
1262
|
-
name: 'Tuya',
|
|
1263
|
-
mode: 'both',
|
|
1264
|
-
options: {
|
|
1265
|
-
projectType: '2',
|
|
1266
|
-
accessId: 'id',
|
|
1267
|
-
accessKey: 'key',
|
|
1268
|
-
countryCode: 1,
|
|
1269
|
-
username: 'user',
|
|
1270
|
-
password: 'pass',
|
|
1271
|
-
appSchema: 'tuyaSmart',
|
|
1272
|
-
generateWeatherAccessory: false,
|
|
1273
|
-
weatherAPI: '',
|
|
1274
|
-
},
|
|
1275
|
-
local: {
|
|
1276
|
-
devices: [],
|
|
1277
|
-
},
|
|
812
|
+
]
|
|
1278
813
|
};
|
|
1279
814
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const mockCloudDevice = {
|
|
1283
|
-
id: 'unique-device-id',
|
|
1284
|
-
uuid: 'unique-device-id',
|
|
1285
|
-
name: 'Device',
|
|
1286
|
-
product_id: 'product-123',
|
|
1287
|
-
} as any;
|
|
1288
|
-
|
|
1289
|
-
bothPlatform.deviceManager = {
|
|
1290
|
-
getDeviceDetails: async () => ({
|
|
1291
|
-
success: true,
|
|
1292
|
-
result: {
|
|
1293
|
-
id: 'unique-device-id',
|
|
1294
|
-
local_key: 'test-key',
|
|
1295
|
-
ip: '192.168.1.1',
|
|
1296
|
-
},
|
|
1297
|
-
}),
|
|
1298
|
-
} as any;
|
|
815
|
+
Object.assign(mockAPI, { updatePlatformAccessories: jest.fn() });
|
|
1299
816
|
|
|
1300
|
-
|
|
817
|
+
const mockPlatform = new TuyaPlatform(mockLog, mockConfig, mockAPI);
|
|
818
|
+
Object.assign(mockPlatform, {
|
|
819
|
+
deviceManager: { createDeviceConfigHash: jest.fn().mockReturnValue('hash1'), devices: []},
|
|
820
|
+
addAccessory: jest.fn().mockReturnValue(mockNewAccessory),
|
|
821
|
+
removeAccessory: jest.fn()
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
mockPlatform.syncDeviceAndAccessory(mockDevice as any, mockOldAccessory as any);
|
|
1301
825
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
826
|
+
const mockRemoveService = jest.spyOn(mockOldAccessory, 'removeService');
|
|
827
|
+
const mockRemoveCharacteristic = jest.spyOn(mockOldAccessory.services[0], 'removeCharacteristic');
|
|
828
|
+
expect(mockRemoveService).toHaveBeenCalledWith(mockOldAccessory.services[1]);
|
|
829
|
+
expect(mockRemoveCharacteristic).toHaveBeenCalledWith(mockOldAccessory.services[0].characteristics[1]);
|
|
1307
830
|
});
|
|
1308
831
|
});
|
|
1309
832
|
});
|