@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,833 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const TuyaDevice_1 = __importStar(require("../cloud/device/TuyaDevice"));
|
|
40
|
+
const TuyaOpenAPI_1 = __importDefault(require("../cloud/api/TuyaOpenAPI"));
|
|
41
|
+
const TuyaDeviceManager_1 = __importDefault(require("../cloud/device/TuyaDeviceManager"));
|
|
42
|
+
const TuyaDiscovery_1 = __importDefault(require("./TuyaDiscovery"));
|
|
43
|
+
const LocalDevice_1 = __importDefault(require("./LocalDevice"));
|
|
44
|
+
const Logger_1 = require("../shared/util/Logger");
|
|
45
|
+
const ConfigHash_1 = require("../shared/util/ConfigHash");
|
|
46
|
+
const ZigbeeGatewayDetection_1 = require("./ZigbeeGatewayDetection");
|
|
47
|
+
const DynamicChildDiscovery_1 = require("./DynamicChildDiscovery");
|
|
48
|
+
/**
|
|
49
|
+
* Default DP-to-code mapping used when the user hasn't supplied one.
|
|
50
|
+
* Keys are Tuya standard instruction codes; values are typical DP numbers.
|
|
51
|
+
* This covers the most common single-switch, dimmer and RGBTW combinations.
|
|
52
|
+
* Users can override via LocalDeviceConfig.dpMapping.
|
|
53
|
+
*/
|
|
54
|
+
const DEFAULT_DP_MAP = {
|
|
55
|
+
switch_1: 1,
|
|
56
|
+
switch_2: 2,
|
|
57
|
+
switch_3: 3,
|
|
58
|
+
switch_4: 4,
|
|
59
|
+
bright_value: 2,
|
|
60
|
+
bright_value_v2: 22,
|
|
61
|
+
temp_value: 3,
|
|
62
|
+
temp_value_v2: 23,
|
|
63
|
+
work_mode: 2,
|
|
64
|
+
colour_data: 5,
|
|
65
|
+
colour_data_v2: 24,
|
|
66
|
+
switch_led: 1,
|
|
67
|
+
countdown_1: 9,
|
|
68
|
+
};
|
|
69
|
+
/** Inverse of DEFAULT_DP_MAP: dp number → code */
|
|
70
|
+
function buildDpToCodeMap(dpMapping) {
|
|
71
|
+
const inv = {};
|
|
72
|
+
for (const [code, dp] of Object.entries(dpMapping)) {
|
|
73
|
+
// Only map first code per dp (prefer explicit mappings)
|
|
74
|
+
if (!(dp in inv)) {
|
|
75
|
+
inv[dp] = code;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return inv;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* LocalDeviceManager – extends TuyaDeviceManager so the existing platform code
|
|
82
|
+
* can treat it (and use its `devices` array / events) without any changes.
|
|
83
|
+
*
|
|
84
|
+
* Uses a dummy TuyaOpenAPI so the parent class initialises without connecting
|
|
85
|
+
* to the Tuya cloud. The MQTT client created by the parent is stopped
|
|
86
|
+
* immediately in the constructor.
|
|
87
|
+
*
|
|
88
|
+
* Local devices are represented as TuyaDevice objects with synthetic schemas
|
|
89
|
+
* derived from the user-supplied (or default) dpMapping. Status is tracked as
|
|
90
|
+
* `{code, value}` pairs, making them compatible with all existing accessory
|
|
91
|
+
* handlers.
|
|
92
|
+
*/
|
|
93
|
+
class LocalDeviceManager extends TuyaDeviceManager_1.default {
|
|
94
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
95
|
+
constructor(localConfig, log, persistPath) {
|
|
96
|
+
// Create a minimal dummy API so the parent initialises without connecting
|
|
97
|
+
const dummyLog = new Logger_1.PrefixLogger(log, 'LocalDummy', false);
|
|
98
|
+
const dummyApi = new TuyaOpenAPI_1.default('', '', '', dummyLog);
|
|
99
|
+
super(dummyApi, false);
|
|
100
|
+
this.localDevices = [];
|
|
101
|
+
this.localConnections = new Map();
|
|
102
|
+
this.discoveryTimeout = null;
|
|
103
|
+
this.rediscoveryTimeout = null;
|
|
104
|
+
this.isDiscoveryActive = false;
|
|
105
|
+
this.discoveredInCurrentPhase = new Set();
|
|
106
|
+
// Maps deviceId → effective dpMapping (code → dp)
|
|
107
|
+
this.dpMaps = new Map();
|
|
108
|
+
// Maps deviceId → reverse dpMapping (dp → code)
|
|
109
|
+
this.reverseDpMaps = new Map();
|
|
110
|
+
// Maps discovered gwId → IP (kept even after TuyaDevice created)
|
|
111
|
+
this.discoveredIPs = new Map();
|
|
112
|
+
// Maps discovered gwId → version
|
|
113
|
+
this.discoveredVersions = new Map();
|
|
114
|
+
// Tracks which devices had config changes
|
|
115
|
+
this.configChanged = new Map();
|
|
116
|
+
// ── Zigbee gateway/child support ──────────────────────────────────────────
|
|
117
|
+
/** Parent gateway ID → GatewayRelationship (derived from config at init). */
|
|
118
|
+
this.gatewayRelationships = new Map();
|
|
119
|
+
/** Parent gateway device ID → its active LocalDevice connection. */
|
|
120
|
+
this.gatewayConnections = new Map();
|
|
121
|
+
// Stop the MQ that the parent started
|
|
122
|
+
this.mq.stop();
|
|
123
|
+
this.parentLog = log;
|
|
124
|
+
this.log = new Logger_1.PrefixLogger(log, 'LocalDeviceManager', false);
|
|
125
|
+
this.config = localConfig;
|
|
126
|
+
this.discovery = new TuyaDiscovery_1.default(log);
|
|
127
|
+
this.configHash = new ConfigHash_1.ConfigHash(persistPath || '', 'tuya-local-configs');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Initialise discovery and register manually configured devices.
|
|
131
|
+
* Returns immediately; discovery events fire asynchronously.
|
|
132
|
+
*/
|
|
133
|
+
async initLocalDevices() {
|
|
134
|
+
// ── Zigbee: detect parent-child relationships before registering devices ──
|
|
135
|
+
if (this.config.devices && this.config.devices.length > 0) {
|
|
136
|
+
try {
|
|
137
|
+
this.gatewayRelationships = ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.detectFromDevices(this.config.devices);
|
|
138
|
+
if (this.gatewayRelationships.size > 0) {
|
|
139
|
+
for (const [parentId, rel] of this.gatewayRelationships) {
|
|
140
|
+
this.log.info(`Zigbee gateway detected: ${parentId} has ${rel.children.length} sub-device(s): ` +
|
|
141
|
+
rel.children.map(c => `${c.name} (CID=${c.cid})`).join(', '));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
147
|
+
this.log.error(`Zigbee configuration error: ${msg}`);
|
|
148
|
+
// Continue without Zigbee – non-fatal for non-Zigbee devices
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Register manual device entries first (non-child devices only at this stage)
|
|
152
|
+
if (this.config.devices) {
|
|
153
|
+
for (const cfg of this.config.devices) {
|
|
154
|
+
// Skip Zigbee children here – they are set up after their parent connects
|
|
155
|
+
if (ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.isChild(cfg)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
this._registerDeviceConfig(cfg);
|
|
159
|
+
}
|
|
160
|
+
// Validate devices have keys if auto-discovery is disabled
|
|
161
|
+
if (this.config.autoDiscoverDevices === false) {
|
|
162
|
+
for (const cfg of this.config.devices) {
|
|
163
|
+
if (!cfg.tuyaKey && !ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.isChild(cfg)) {
|
|
164
|
+
const deviceName = cfg.name || cfg.tuyaDeviceId;
|
|
165
|
+
this.log.warn(`Local device "${deviceName}" (${cfg.tuyaDeviceId}) is missing tuyaKey ` +
|
|
166
|
+
'and auto-discovery is disabled. This device will not be usable.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Start UDP discovery unless explicitly disabled
|
|
172
|
+
if (this.config.autoDiscoverDevices !== false) {
|
|
173
|
+
this.discovery.on('discover', (result) => {
|
|
174
|
+
this._onDiscovered(result);
|
|
175
|
+
});
|
|
176
|
+
this._startDiscoveryPhase();
|
|
177
|
+
// Schedule periodic rediscovery to handle devices that go offline and return
|
|
178
|
+
// (e.g., after WiFi reset or IP change)
|
|
179
|
+
const rediscoverIntervalSec = this.config.rediscoverInterval ?? 900; // Default 15 minutes
|
|
180
|
+
if (rediscoverIntervalSec > 0) {
|
|
181
|
+
this.log.info(`Scheduling periodic device rediscovery every ${rediscoverIntervalSec} seconds`);
|
|
182
|
+
this._schedulePeriodicRediscovery(rediscoverIntervalSec);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Stop discovery and disconnect all local TCP connections. */
|
|
187
|
+
stopLocalDevices() {
|
|
188
|
+
if (this.discoveryTimeout) {
|
|
189
|
+
clearTimeout(this.discoveryTimeout);
|
|
190
|
+
this.discoveryTimeout = null;
|
|
191
|
+
}
|
|
192
|
+
if (this.rediscoveryTimeout) {
|
|
193
|
+
clearTimeout(this.rediscoveryTimeout);
|
|
194
|
+
this.rediscoveryTimeout = null;
|
|
195
|
+
}
|
|
196
|
+
this.discovery.stop();
|
|
197
|
+
this.isDiscoveryActive = false;
|
|
198
|
+
for (const ld of this.localConnections.values()) {
|
|
199
|
+
ld.disconnect();
|
|
200
|
+
}
|
|
201
|
+
this.localConnections.clear();
|
|
202
|
+
// Gateway connections are separate from localConnections
|
|
203
|
+
for (const gw of this.gatewayConnections.values()) {
|
|
204
|
+
gw.disconnect();
|
|
205
|
+
}
|
|
206
|
+
this.gatewayConnections.clear();
|
|
207
|
+
}
|
|
208
|
+
/** Returns ALL devices (both local shadow entries from this manager). */
|
|
209
|
+
getDevice(deviceID) {
|
|
210
|
+
return this.localDevices.find(d => d.id === deviceID);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Send commands to a local device.
|
|
214
|
+
* `commands` is the same [{code, value}] format used by the cloud API so
|
|
215
|
+
* all existing accessory handlers work without modification.
|
|
216
|
+
*/
|
|
217
|
+
async sendCommands(deviceID, commands) {
|
|
218
|
+
const dpMap = this.dpMaps.get(deviceID);
|
|
219
|
+
if (!dpMap) {
|
|
220
|
+
this.log.warn(`No dpMapping for local device ${deviceID}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const dps = {};
|
|
224
|
+
for (const { code, value } of commands) {
|
|
225
|
+
const dp = dpMap[code];
|
|
226
|
+
if (dp === undefined) {
|
|
227
|
+
this.log.warn(`Unknown code "${code}" for device ${deviceID}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
dps[String(dp)] = value;
|
|
231
|
+
}
|
|
232
|
+
if (Object.keys(dps).length === 0) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Log the command for user visibility
|
|
236
|
+
const device = this.localDevices.find(d => d.id === deviceID);
|
|
237
|
+
const deviceName = device?.name || deviceID;
|
|
238
|
+
const commandStr = commands.map(c => `${c.code}=${c.value}`).join(', ');
|
|
239
|
+
this.log.info(`[${deviceName}] Sending command (local): ${commandStr}`);
|
|
240
|
+
// ── Zigbee child path ────────────────────────────────────────────────
|
|
241
|
+
const childConn = this.localConnections.get(deviceID);
|
|
242
|
+
if (childConn?.parentDevice) {
|
|
243
|
+
// Already set up as a Zigbee child – LocalDevice.update() delegates to parent
|
|
244
|
+
childConn.update(dps);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
248
|
+
let conn = this.localConnections.get(deviceID);
|
|
249
|
+
if (!conn) {
|
|
250
|
+
conn = this._createConnection(deviceID);
|
|
251
|
+
if (!conn) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
conn.update(dps);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
259
|
+
/**
|
|
260
|
+
* Start a discovery phase (e.g. initial startup or periodic rediscovery).
|
|
261
|
+
* Runs discovery for discoverTimeout seconds, then stops.
|
|
262
|
+
*/
|
|
263
|
+
_startDiscoveryPhase() {
|
|
264
|
+
if (this.isDiscoveryActive) {
|
|
265
|
+
return; // Discovery already running
|
|
266
|
+
}
|
|
267
|
+
this.isDiscoveryActive = true;
|
|
268
|
+
this.discoveredInCurrentPhase.clear();
|
|
269
|
+
this.log.info('Starting discovery phase');
|
|
270
|
+
this.discovery.start();
|
|
271
|
+
// Stop after configurable timeout (config is in seconds, default 60s)
|
|
272
|
+
// Convert to milliseconds
|
|
273
|
+
const timeoutMs = (this.config.discoverTimeout ?? 60) * 1000;
|
|
274
|
+
if (this.discoveryTimeout) {
|
|
275
|
+
clearTimeout(this.discoveryTimeout);
|
|
276
|
+
}
|
|
277
|
+
this.discoveryTimeout = setTimeout(() => {
|
|
278
|
+
this.isDiscoveryActive = false;
|
|
279
|
+
this.discovery.stop();
|
|
280
|
+
// Report discovery results for clarity
|
|
281
|
+
if (this.discoveredInCurrentPhase.size > 0) {
|
|
282
|
+
this.log.info(`Ended discovery phase – rediscovered ${this.discoveredInCurrentPhase.size} device(s)`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// No devices found; they may be using cached IPs from previous discovery
|
|
286
|
+
const knownDevices = this.localDevices.filter(d => d.localKey);
|
|
287
|
+
if (knownDevices.length > 0) {
|
|
288
|
+
this.log.info(`Ended discovery phase – no new devices found (${knownDevices.length} configured device(s) using cached IP)`);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
this.log.info('Ended discovery phase – no devices found');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
this.discoveryTimeout = null;
|
|
295
|
+
}, timeoutMs);
|
|
296
|
+
this.discoveryTimeout.unref();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Schedule periodic rediscovery to detect devices that go offline and return.
|
|
300
|
+
* This helps handle WiFi resets, IP changes, and network reconnections.
|
|
301
|
+
*/
|
|
302
|
+
_schedulePeriodicRediscovery(intervalSec) {
|
|
303
|
+
if (this.rediscoveryTimeout) {
|
|
304
|
+
clearTimeout(this.rediscoveryTimeout);
|
|
305
|
+
}
|
|
306
|
+
const intervalMs = intervalSec * 1000;
|
|
307
|
+
this.rediscoveryTimeout = setTimeout(() => {
|
|
308
|
+
this.log.debug('Triggering periodic device rediscovery');
|
|
309
|
+
this._startDiscoveryPhase();
|
|
310
|
+
// Reschedule for the next cycle
|
|
311
|
+
this._schedulePeriodicRediscovery(intervalSec);
|
|
312
|
+
}, intervalMs);
|
|
313
|
+
this.rediscoveryTimeout.unref();
|
|
314
|
+
}
|
|
315
|
+
_registerDeviceConfig(cfg) {
|
|
316
|
+
const effectiveMap = Object.assign({}, DEFAULT_DP_MAP, cfg.dpMapping ?? {});
|
|
317
|
+
this.dpMaps.set(cfg.tuyaDeviceId, effectiveMap);
|
|
318
|
+
this.reverseDpMaps.set(cfg.tuyaDeviceId, buildDpToCodeMap(effectiveMap));
|
|
319
|
+
const existing = this.localDevices.find(d => d.id === cfg.tuyaDeviceId);
|
|
320
|
+
// Check if config has changed since last run
|
|
321
|
+
// Hash the config fields that affect device behavior
|
|
322
|
+
const configToHash = {
|
|
323
|
+
tuyaDeviceId: cfg.tuyaDeviceId,
|
|
324
|
+
name: cfg.name,
|
|
325
|
+
tuyaKey: cfg.tuyaKey,
|
|
326
|
+
dpMapping: cfg.dpMapping,
|
|
327
|
+
switchCount: cfg.switchCount,
|
|
328
|
+
outletCount: cfg.outletCount,
|
|
329
|
+
protocolVersion: cfg.protocolVersion,
|
|
330
|
+
category: cfg.category,
|
|
331
|
+
};
|
|
332
|
+
const { changed: configChanged } = this.configHash.hasConfigChanged(cfg.tuyaDeviceId, configToHash);
|
|
333
|
+
this.configChanged.set(cfg.tuyaDeviceId, configChanged);
|
|
334
|
+
if (configChanged && existing) {
|
|
335
|
+
this.log.info(`Device ${cfg.tuyaDeviceId}: config changed, rebuilding schema`);
|
|
336
|
+
}
|
|
337
|
+
if (!existing) {
|
|
338
|
+
const device = this._buildTuyaDevice(cfg, configChanged);
|
|
339
|
+
this.localDevices.push(device);
|
|
340
|
+
this.devices.push(device); // keep parent's devices array in sync
|
|
341
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_ADD, device);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Set up auto-detection listener to count switches from device status updates.
|
|
346
|
+
* Called BEFORE conn.connect() to avoid race conditions.
|
|
347
|
+
* Uses time-based accumulation to capture all switches across multiple updates.
|
|
348
|
+
*/
|
|
349
|
+
_setupAutoDetectionListener(deviceId, device, conn) {
|
|
350
|
+
const cfg = this.config.devices?.find(d => d.tuyaDeviceId === deviceId);
|
|
351
|
+
if (!cfg || (cfg.switchCount || cfg.outletCount)) {
|
|
352
|
+
return; // Already has explicit count
|
|
353
|
+
}
|
|
354
|
+
this.log.debug(`[AutoDetect] Setting up auto-detection listener for ${deviceId}`);
|
|
355
|
+
const detectedSwitches = new Set();
|
|
356
|
+
let detectionTimer = null;
|
|
357
|
+
const onStatusChange = (changes) => {
|
|
358
|
+
this.log.debug(`[AutoDetect] Status change received for ${deviceId}: ${JSON.stringify(Object.keys(changes))}`);
|
|
359
|
+
// Collect switch numbers from incoming changes
|
|
360
|
+
const dpMap = this.dpMaps.get(deviceId) ?? DEFAULT_DP_MAP;
|
|
361
|
+
const reverseMap = buildDpToCodeMap(dpMap);
|
|
362
|
+
for (const dpStr of Object.keys(changes)) {
|
|
363
|
+
const dpNum = parseInt(dpStr, 10);
|
|
364
|
+
const code = reverseMap[dpNum];
|
|
365
|
+
const switchMatch = code?.match(/^switch_(\d+)$/);
|
|
366
|
+
if (switchMatch) {
|
|
367
|
+
const switchNum = parseInt(switchMatch[1], 10);
|
|
368
|
+
detectedSwitches.add(switchNum);
|
|
369
|
+
this.log.debug(`[AutoDetect] Found switch_${switchNum} (dp=${dpNum}, code=${code})`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Restart the detection timer
|
|
373
|
+
if (detectionTimer) {
|
|
374
|
+
clearTimeout(detectionTimer);
|
|
375
|
+
}
|
|
376
|
+
detectionTimer = setTimeout(() => {
|
|
377
|
+
// After 2 seconds of no new switches, finalize auto-detection
|
|
378
|
+
if (detectedSwitches.size > 0) {
|
|
379
|
+
const detectedCount = Math.max(...Array.from(detectedSwitches));
|
|
380
|
+
const switchList = Array.from(detectedSwitches).sort((a, b) => a - b).join(', ');
|
|
381
|
+
this.log.info(`[AutoDetect] Auto-detected ${detectedCount} switch(es) for "${cfg.name}" (${deviceId}): ` +
|
|
382
|
+
`switches=[${switchList}]`);
|
|
383
|
+
// Update device schema with detected switch count
|
|
384
|
+
device.schema = this._buildSyntheticSchema(this.dpMaps.get(deviceId) ?? DEFAULT_DP_MAP, detectedCount);
|
|
385
|
+
device.switchCount = detectedCount;
|
|
386
|
+
// Auto-detection is now complete
|
|
387
|
+
device.isAutoDetecting = false;
|
|
388
|
+
// Recalculate config hash since the schema has changed
|
|
389
|
+
// This ensures SwitchAccessory knows to rebuild services
|
|
390
|
+
const updatedConfigToHash = {
|
|
391
|
+
tuyaDeviceId: cfg.tuyaDeviceId,
|
|
392
|
+
name: cfg.name,
|
|
393
|
+
tuyaKey: cfg.tuyaKey,
|
|
394
|
+
dpMapping: cfg.dpMapping,
|
|
395
|
+
switchCount: detectedCount,
|
|
396
|
+
outletCount: cfg.outletCount,
|
|
397
|
+
protocolVersion: cfg.protocolVersion,
|
|
398
|
+
category: cfg.category,
|
|
399
|
+
};
|
|
400
|
+
const { changed: schemaChanged } = this.configHash.hasConfigChanged(deviceId, updatedConfigToHash);
|
|
401
|
+
if (schemaChanged) {
|
|
402
|
+
this.log.debug(`[AutoDetect] Schema changed after auto-detection, marking config as changed`);
|
|
403
|
+
device.configChanged = true;
|
|
404
|
+
this.configChanged.set(deviceId, true);
|
|
405
|
+
}
|
|
406
|
+
// Notify the platform so configureServices() is re-run and switch_2+ services are added.
|
|
407
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, device, { schemaUpdated: true });
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
this.log.debug(`[AutoDetect] No switches detected after 2 seconds for ${deviceId}`);
|
|
411
|
+
}
|
|
412
|
+
// Remove listener after detection is complete
|
|
413
|
+
conn.removeListener('change', onStatusChange);
|
|
414
|
+
}, 2000);
|
|
415
|
+
};
|
|
416
|
+
// Listen for status changes (multiple events to capture all switches)
|
|
417
|
+
conn.on('change', onStatusChange);
|
|
418
|
+
this.log.debug(`[AutoDetect] Listener attached to connection for ${deviceId}`);
|
|
419
|
+
}
|
|
420
|
+
_onDiscovered(result) {
|
|
421
|
+
this.discoveredIPs.set(result.id, result.ip);
|
|
422
|
+
this.discoveredVersions.set(result.id, result.version);
|
|
423
|
+
this.discoveredInCurrentPhase.add(result.id);
|
|
424
|
+
// If we already have a TuyaDevice for this ID (from manual config), update its IP
|
|
425
|
+
const existing = this.localDevices.find(d => d.id === result.id);
|
|
426
|
+
if (existing) {
|
|
427
|
+
this.log.info(`Device ${result.id} (v${result.version}) rediscovered @ ${result.ip} ` +
|
|
428
|
+
'– updating connection');
|
|
429
|
+
existing.ip = result.ip;
|
|
430
|
+
// Update protocol version if not user-configured
|
|
431
|
+
const typed = existing;
|
|
432
|
+
if (!typed.cfgVersion) {
|
|
433
|
+
typed.localVersion = result.version;
|
|
434
|
+
}
|
|
435
|
+
// If a connection exists, reconnect with updated IP/version
|
|
436
|
+
const conn = this.localConnections.get(result.id);
|
|
437
|
+
if (conn) {
|
|
438
|
+
conn.disconnect();
|
|
439
|
+
this.localConnections.delete(result.id);
|
|
440
|
+
}
|
|
441
|
+
// Try to establish a connection if the device has a local key
|
|
442
|
+
const localKey = existing.localKey;
|
|
443
|
+
if (localKey) {
|
|
444
|
+
this._createConnection(result.id);
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// New device discovered via UDP – create a placeholder TuyaDevice
|
|
449
|
+
// (it won't be controllable until the user provides a tuyaKey via config)
|
|
450
|
+
const device = this._buildDiscoveredDevice(result);
|
|
451
|
+
this.localDevices.push(device);
|
|
452
|
+
this.devices.push(device);
|
|
453
|
+
const productInfo = result.productKey ? ` productKey=${result.productKey}` : '';
|
|
454
|
+
this.log.info(`Local device discovered: ${result.id} @ ${result.ip} (v${result.version}${productInfo}) ` +
|
|
455
|
+
'– add to config with tuyaKey to control');
|
|
456
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_ADD, device);
|
|
457
|
+
}
|
|
458
|
+
_buildTuyaDevice(cfg, configChanged = false) {
|
|
459
|
+
const device = new TuyaDevice_1.default({});
|
|
460
|
+
device.id = cfg.tuyaDeviceId;
|
|
461
|
+
device.uuid = cfg.tuyaDeviceId;
|
|
462
|
+
device.name = cfg.name ?? `LocalDevice-${cfg.tuyaDeviceId.slice(0, 8)}`;
|
|
463
|
+
device.category = cfg.category ?? 'unknown';
|
|
464
|
+
device.ip = cfg.ip ?? (this.discoveredIPs.get(cfg.tuyaDeviceId) ?? '');
|
|
465
|
+
device.online = false;
|
|
466
|
+
device.product_id = '';
|
|
467
|
+
device.product_name = 'Local Device';
|
|
468
|
+
device.model = '';
|
|
469
|
+
device.icon = '';
|
|
470
|
+
device.owner_id = '';
|
|
471
|
+
device.lat = '';
|
|
472
|
+
device.lon = '';
|
|
473
|
+
device.time_zone = '';
|
|
474
|
+
device.create_time = 0;
|
|
475
|
+
device.active_time = 0;
|
|
476
|
+
device.update_time = 0;
|
|
477
|
+
device.status = [];
|
|
478
|
+
const switchCount = cfg.switchCount ?? cfg.outletCount ?? 1;
|
|
479
|
+
const autoDetectNote = !cfg.switchCount && !cfg.outletCount && cfg.tuyaKey ? ' (auto-detecting on connect)' : '';
|
|
480
|
+
const isAutoDetecting = !!(cfg.tuyaKey && !cfg.switchCount && !cfg.outletCount);
|
|
481
|
+
this.log.info(`Building synthetic schema for ${cfg.name} with switchCount=${switchCount}${autoDetectNote}`);
|
|
482
|
+
device.schema = this._buildSyntheticSchema(Object.assign({}, DEFAULT_DP_MAP, cfg.dpMapping ?? {}), switchCount);
|
|
483
|
+
this.log.info(`Device "${cfg.name}" schema includes ${device.schema.length} codes: ${device.schema.map(s => s.code).join(', ')}`);
|
|
484
|
+
// Store local-specific metadata directly on the device object for later use
|
|
485
|
+
device.localKey = cfg.tuyaKey;
|
|
486
|
+
device.localIp = cfg.ip;
|
|
487
|
+
device.localVersion =
|
|
488
|
+
cfg.protocolVersion ?? this.discoveredVersions.get(cfg.tuyaDeviceId);
|
|
489
|
+
// Track whether version was user-configured (prevents overwrite on re-discovery)
|
|
490
|
+
device.cfgVersion = cfg.protocolVersion;
|
|
491
|
+
device.isLocal = true;
|
|
492
|
+
device.switchCount = switchCount;
|
|
493
|
+
device.isAutoDetecting = isAutoDetecting;
|
|
494
|
+
// Track whether config changed (used by SwitchAccessory to decide whether to remove cached extras)
|
|
495
|
+
device.configChanged = configChanged;
|
|
496
|
+
return device;
|
|
497
|
+
}
|
|
498
|
+
_buildDiscoveredDevice(result) {
|
|
499
|
+
const device = new TuyaDevice_1.default({});
|
|
500
|
+
device.id = result.id;
|
|
501
|
+
device.uuid = result.id;
|
|
502
|
+
device.name = `Local-${result.id.slice(0, 8)}`;
|
|
503
|
+
device.category = 'unknown';
|
|
504
|
+
device.ip = result.ip;
|
|
505
|
+
device.online = false;
|
|
506
|
+
device.product_id = result.productKey ?? '';
|
|
507
|
+
device.product_name = 'Local Device';
|
|
508
|
+
device.model = '';
|
|
509
|
+
device.icon = '';
|
|
510
|
+
device.owner_id = '';
|
|
511
|
+
device.lat = '';
|
|
512
|
+
device.lon = '';
|
|
513
|
+
device.time_zone = '';
|
|
514
|
+
device.create_time = 0;
|
|
515
|
+
device.active_time = 0;
|
|
516
|
+
device.update_time = 0;
|
|
517
|
+
device.status = [];
|
|
518
|
+
device.schema = [];
|
|
519
|
+
device.isLocal = true;
|
|
520
|
+
device.localVersion = result.version;
|
|
521
|
+
return device;
|
|
522
|
+
}
|
|
523
|
+
_buildSyntheticSchema(dpMapping, switchCount = 1) {
|
|
524
|
+
// If switchCount is limited, filter switch-related entries to only the needed ones
|
|
525
|
+
let filteredMapping = dpMapping;
|
|
526
|
+
if (switchCount > 0) {
|
|
527
|
+
// Only include switch_1 through switch_N where N = switchCount
|
|
528
|
+
// Also exclude other switch variants (like switch_led, switch_inching) if limited
|
|
529
|
+
filteredMapping = {};
|
|
530
|
+
const switchCodes = [];
|
|
531
|
+
for (const [code, dp] of Object.entries(dpMapping)) {
|
|
532
|
+
// Keep non-switch DPs (brightness, temp, color, etc.)
|
|
533
|
+
if (!code.startsWith('switch')) {
|
|
534
|
+
filteredMapping[code] = dp;
|
|
535
|
+
}
|
|
536
|
+
// For switches, only include switch_1..switch_N
|
|
537
|
+
const switchMatch = code.match(/^switch_(\d+)$/);
|
|
538
|
+
if (switchMatch) {
|
|
539
|
+
const switchNum = parseInt(switchMatch[1], 10);
|
|
540
|
+
if (switchNum <= switchCount) {
|
|
541
|
+
filteredMapping[code] = dp;
|
|
542
|
+
switchCodes.push(code);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (switchCodes.length < Object.keys(dpMapping).filter(k => k.startsWith('switch')).length) {
|
|
547
|
+
this.log.debug(`Filtered switches: kept [${switchCodes.join(', ')}], excluded others`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return Object.keys(filteredMapping).map(code => ({
|
|
551
|
+
code,
|
|
552
|
+
mode: TuyaDevice_1.TuyaDeviceSchemaMode.READ_WRITE,
|
|
553
|
+
type: TuyaDevice_1.TuyaDeviceSchemaType.Boolean, // best-effort default; handlers may override
|
|
554
|
+
property: {},
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
_createConnection(deviceID) {
|
|
558
|
+
const device = this.localDevices.find(d => d.id === deviceID);
|
|
559
|
+
if (!device) {
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
const localKey = device.localKey;
|
|
563
|
+
if (!localKey) {
|
|
564
|
+
this.log.warn(`No tuyaKey configured for local device ${deviceID} — cannot connect.`);
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
const ip = device.ip || device.localIp;
|
|
568
|
+
if (!ip) {
|
|
569
|
+
this.log.warn(`No IP known for local device ${deviceID} — cannot connect.`);
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
const version = device.localVersion
|
|
573
|
+
?? this.discoveredVersions.get(deviceID)
|
|
574
|
+
?? '3.3';
|
|
575
|
+
const conn = new LocalDevice_1.default({
|
|
576
|
+
id: deviceID,
|
|
577
|
+
key: Buffer.from(localKey, 'utf8'),
|
|
578
|
+
ip,
|
|
579
|
+
version,
|
|
580
|
+
name: device.name,
|
|
581
|
+
}, this.parentLog);
|
|
582
|
+
conn.on('connect', () => {
|
|
583
|
+
device.online = true;
|
|
584
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, device, { online: true });
|
|
585
|
+
// ── Zigbee: when a gateway connects, set up its children ──
|
|
586
|
+
if (this.gatewayRelationships.has(deviceID)) {
|
|
587
|
+
this.gatewayConnections.set(deviceID, conn);
|
|
588
|
+
this._setupZigbeeChildren(deviceID, conn);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
conn.on('disconnect', () => {
|
|
592
|
+
device.online = false;
|
|
593
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, device, { online: false });
|
|
594
|
+
});
|
|
595
|
+
conn.on('change', (changes) => {
|
|
596
|
+
const reverseMap = this.reverseDpMaps.get(deviceID) ?? {};
|
|
597
|
+
const statusUpdates = [];
|
|
598
|
+
for (const [dp, val] of Object.entries(changes)) {
|
|
599
|
+
const code = reverseMap[parseInt(dp, 10)] ?? dp;
|
|
600
|
+
// Update device.status array
|
|
601
|
+
const existing = device.status.find(s => s.code === code);
|
|
602
|
+
if (existing) {
|
|
603
|
+
existing.value = val;
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
device.status.push({ code, value: val });
|
|
607
|
+
}
|
|
608
|
+
statusUpdates.push({ code, value: val });
|
|
609
|
+
}
|
|
610
|
+
if (statusUpdates.length > 0) {
|
|
611
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, device, statusUpdates);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
conn.on('error', (err) => {
|
|
615
|
+
this.log.warn(`Error for local device ${deviceID}: ${err.message}`);
|
|
616
|
+
});
|
|
617
|
+
// Set up auto-detection listener BEFORE connecting to avoid race conditions
|
|
618
|
+
this._setupAutoDetectionListener(deviceID, device, conn);
|
|
619
|
+
conn.connect();
|
|
620
|
+
this.localConnections.set(deviceID, conn);
|
|
621
|
+
return conn;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* After a Zigbee gateway connects, configure all its child sub-devices.
|
|
625
|
+
* Each child gets a lightweight LocalDevice whose update() call delegates
|
|
626
|
+
* to the parent, with the CID embedded in the payload.
|
|
627
|
+
*
|
|
628
|
+
* If a child connection already exists (e.g. after a gateway reconnect),
|
|
629
|
+
* re-link it to the parent and restore its connected state.
|
|
630
|
+
*/
|
|
631
|
+
_setupZigbeeChildren(parentId, parentConn) {
|
|
632
|
+
const rel = this.gatewayRelationships.get(parentId);
|
|
633
|
+
if (!rel)
|
|
634
|
+
return;
|
|
635
|
+
for (const entry of rel.children) {
|
|
636
|
+
const existingConn = this.localConnections.get(entry.deviceId);
|
|
637
|
+
if (existingConn && existingConn.parentDevice) {
|
|
638
|
+
// Child connection already exists – re-bind to new parent connection
|
|
639
|
+
this._reconnectZigbeeChild(entry.deviceId, entry.cid, existingConn, parentConn);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// New child – set up from scratch
|
|
643
|
+
this._setupZigbeeChild(entry.deviceId, entry.cid, parentConn);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Re-bind an existing Zigbee child connection to a newly reconnected parent gateway.
|
|
649
|
+
* Preserves the child's state and event listeners while updating the parent reference.
|
|
650
|
+
*/
|
|
651
|
+
_reconnectZigbeeChild(childDeviceId, cid, childConn, newParentConn) {
|
|
652
|
+
const childTuyaDevice = this.localDevices.find(d => d.id === childDeviceId);
|
|
653
|
+
if (!childTuyaDevice) {
|
|
654
|
+
this.log.warn(`Zigbee child ${childDeviceId}: TuyaDevice not found during reconnect`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// Update parent reference and CID
|
|
658
|
+
childConn.parentDevice = newParentConn;
|
|
659
|
+
childConn.childId = cid;
|
|
660
|
+
newParentConn.children.set(cid, childConn);
|
|
661
|
+
// Mark child as connected (mirrors parent state)
|
|
662
|
+
childConn.connected = true;
|
|
663
|
+
this.log.info(`Zigbee child ${childDeviceId} (CID=${cid}): re-linked to parent on reconnect`);
|
|
664
|
+
// Notify that device is now online
|
|
665
|
+
childTuyaDevice.online = true;
|
|
666
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: true });
|
|
667
|
+
// Request fresh state from child via parent
|
|
668
|
+
newParentConn.queryStateChild(cid);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Set up a single Zigbee child device.
|
|
672
|
+
* Registers the child's TuyaDevice (if not already done), links it to the
|
|
673
|
+
* parent LocalDevice connection, and subscribes to status updates.
|
|
674
|
+
*/
|
|
675
|
+
_setupZigbeeChild(childDeviceId, cid, parentConn) {
|
|
676
|
+
const cfg = this.config.devices?.find(d => d.tuyaDeviceId === childDeviceId);
|
|
677
|
+
if (!cfg) {
|
|
678
|
+
this.log.warn(`Zigbee child config not found for device ${childDeviceId}`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// Check if child has per-child overrides for DP mapping or category
|
|
682
|
+
const effectiveChildCfg = { ...cfg };
|
|
683
|
+
if (cfg.childDpMapping) {
|
|
684
|
+
effectiveChildCfg.dpMapping = cfg.childDpMapping;
|
|
685
|
+
this.log.debug(`Zigbee child ${childDeviceId}: using per-child DP mapping override`);
|
|
686
|
+
}
|
|
687
|
+
if (cfg.childCategory) {
|
|
688
|
+
effectiveChildCfg.category = cfg.childCategory;
|
|
689
|
+
this.log.debug(`Zigbee child ${childDeviceId}: using per-child category override`);
|
|
690
|
+
}
|
|
691
|
+
// Register TuyaDevice if not already present
|
|
692
|
+
if (!this.localDevices.find(d => d.id === childDeviceId)) {
|
|
693
|
+
this._registerDeviceConfig(effectiveChildCfg);
|
|
694
|
+
}
|
|
695
|
+
const childTuyaDevice = this.localDevices.find(d => d.id === childDeviceId);
|
|
696
|
+
if (!childTuyaDevice) {
|
|
697
|
+
this.log.warn(`Could not build TuyaDevice for Zigbee child ${childDeviceId}`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// Create a lightweight LocalDevice for the child (no socket of its own)
|
|
701
|
+
const childConn = new LocalDevice_1.default({
|
|
702
|
+
id: childDeviceId,
|
|
703
|
+
key: parentConn['context'].key,
|
|
704
|
+
ip: parentConn['context'].ip,
|
|
705
|
+
version: parentConn['context'].version,
|
|
706
|
+
name: cfg.name ?? childDeviceId,
|
|
707
|
+
}, this.parentLog);
|
|
708
|
+
// Link parent ↔ child
|
|
709
|
+
childConn.parentDevice = parentConn;
|
|
710
|
+
childConn.childId = cid;
|
|
711
|
+
parentConn.children.set(cid, childConn);
|
|
712
|
+
// Mark child as connected immediately (it's online whenever parent is)
|
|
713
|
+
childConn.connected = true;
|
|
714
|
+
childConn.on('disconnect', () => {
|
|
715
|
+
childTuyaDevice.online = false;
|
|
716
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: false });
|
|
717
|
+
});
|
|
718
|
+
childConn.on('change', (changes) => {
|
|
719
|
+
const reverseMap = this.reverseDpMaps.get(childDeviceId) ?? {};
|
|
720
|
+
const statusUpdates = [];
|
|
721
|
+
for (const [dp, val] of Object.entries(changes)) {
|
|
722
|
+
const code = reverseMap[parseInt(dp, 10)] ?? dp;
|
|
723
|
+
const existing = childTuyaDevice.status.find(s => s.code === code);
|
|
724
|
+
if (existing) {
|
|
725
|
+
existing.value = val;
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
childTuyaDevice.status.push({ code, value: val });
|
|
729
|
+
}
|
|
730
|
+
statusUpdates.push({ code, value: val });
|
|
731
|
+
}
|
|
732
|
+
if (statusUpdates.length > 0) {
|
|
733
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_STATUS_UPDATE, childTuyaDevice, statusUpdates);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
// Also handle the 'connect' event that parent propagation may emit
|
|
737
|
+
parentConn.on('connect', () => {
|
|
738
|
+
if (!childConn.connected) {
|
|
739
|
+
childConn.connected = true;
|
|
740
|
+
childTuyaDevice.online = true;
|
|
741
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: true });
|
|
742
|
+
// Refresh child state after gateway reconnects
|
|
743
|
+
parentConn.queryStateChild(cid);
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
this.localConnections.set(childDeviceId, childConn);
|
|
747
|
+
childTuyaDevice.online = true;
|
|
748
|
+
this.emit(TuyaDeviceManager_1.default.Events.DEVICE_INFO_UPDATE, childTuyaDevice, { online: true });
|
|
749
|
+
// Request child's current state immediately
|
|
750
|
+
parentConn.queryStateChild(cid);
|
|
751
|
+
this.log.info(`Zigbee child "${cfg.name ?? childDeviceId}" (${childDeviceId}, CID=${cid}) ` +
|
|
752
|
+
`connected via gateway ${parentConn['context'].id}`);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Discover and register Zigbee children from Tuya Cloud device list.
|
|
756
|
+
* If cloud API provides gateway_id field, we can auto-detect parent-child relationships
|
|
757
|
+
* and create child configs without manual config.
|
|
758
|
+
*
|
|
759
|
+
* @param cloudDeviceList - Optional array of cloud devices with id and gateway_id fields
|
|
760
|
+
*/
|
|
761
|
+
discoverChildrenFromCloud(cloudDeviceList) {
|
|
762
|
+
if (!cloudDeviceList || cloudDeviceList.length === 0) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// Find parent-child relationships from cloud metadata
|
|
766
|
+
const gatewayToChildren = (0, DynamicChildDiscovery_1.discoverFromCloudList)(cloudDeviceList);
|
|
767
|
+
if (gatewayToChildren.size === 0) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
// For each discovered parent-child relationship, ensure child config exists
|
|
771
|
+
for (const [gatewayDeviceId, childDeviceIds] of gatewayToChildren) {
|
|
772
|
+
const parentCfg = this.config.devices?.find(d => d.tuyaDeviceId === gatewayDeviceId);
|
|
773
|
+
if (!parentCfg) {
|
|
774
|
+
this.log.debug(`Cloud parent ${gatewayDeviceId}: no local config found, skipping auto-discovery`);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
// Only auto-discover for gateways that support it
|
|
778
|
+
if (!(0, DynamicChildDiscovery_1.supportsChildDiscovery)(parentCfg)) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
for (const childDeviceId of childDeviceIds) {
|
|
782
|
+
// Check if child config already exists
|
|
783
|
+
const existingChildCfg = this.config.devices?.find(d => d.tuyaDeviceId === childDeviceId);
|
|
784
|
+
if (existingChildCfg) {
|
|
785
|
+
continue; // Already configured manually
|
|
786
|
+
}
|
|
787
|
+
// Auto-create child config from cloud metadata
|
|
788
|
+
// Use a placeholder CID (will be replaced by LAN discovery when gateway connects)
|
|
789
|
+
const placeholderCid = `00000000${childDeviceId.substring(0, 8).toLowerCase().padEnd(8, '0')}`;
|
|
790
|
+
const autoChildCfg = (0, DynamicChildDiscovery_1.buildDiscoveredChildConfig)(gatewayDeviceId, placeholderCid, undefined, parentCfg.ip, parentCfg.tuyaKey);
|
|
791
|
+
autoChildCfg.zigbeeChildId = undefined; // Will be discovered dynamically
|
|
792
|
+
if (!this.config.devices) {
|
|
793
|
+
this.config.devices = [];
|
|
794
|
+
}
|
|
795
|
+
this.config.devices.push(autoChildCfg);
|
|
796
|
+
this.log.info(`Zigbee child auto-discovery: registered ${childDeviceId} as child of ${gatewayDeviceId} ` +
|
|
797
|
+
'(from cloud metadata)');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Re-detect Zigbee relationships now that we've added auto-discovered children
|
|
801
|
+
if (this.config.devices && this.config.devices.length > 0) {
|
|
802
|
+
try {
|
|
803
|
+
this.gatewayRelationships = ZigbeeGatewayDetection_1.ZigbeeGatewayDetection.detectFromDevices(this.config.devices);
|
|
804
|
+
}
|
|
805
|
+
catch (err) {
|
|
806
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
807
|
+
this.log.warn(`Re-detection of Zigbee relationships after auto-discovery failed: ${msg}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/** Connect all configured local devices eagerly (called after init). */
|
|
812
|
+
connectAllDevices() {
|
|
813
|
+
for (const device of this.localDevices) {
|
|
814
|
+
const localKey = device.localKey;
|
|
815
|
+
if (!localKey) {
|
|
816
|
+
continue; // Skip if no local key configured
|
|
817
|
+
}
|
|
818
|
+
// Only connect if device has an explicit IP in config
|
|
819
|
+
// Devices relying on auto-discovery will be connected later after discovery completes
|
|
820
|
+
const localIp = device.localIp;
|
|
821
|
+
if (!localIp) {
|
|
822
|
+
this.log.debug(`Skipping initial connection for ${device.name} (${device.id}): ` +
|
|
823
|
+
'waiting for auto-discovery to find IP address');
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (device.ip) {
|
|
827
|
+
this._createConnection(device.id);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
exports.default = LocalDeviceManager;
|
|
833
|
+
//# sourceMappingURL=LocalDeviceManager.js.map
|