From 0afbef7f1110b05a2ca03dbac2be70911d0bf604 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Wed, 27 Apr 2022 21:37:40 +0200 Subject: [PATCH] Fix/fix fan discovery (#21) * added LAP-C601S-WUS (Core 600s Air Purifier) * fix style * better fix to diagnostic. * fix preset mode * add a log line. * add another debug log * add some missing fan devices. * auto discover preset_modes * auto discover fan speed range * fix fan speed and preset modes * fix fan speed number * add some debug log. * merge fix from main * rename a key in diagnostics. --- custom_components/vesync/binary_sensor.py | 6 ++- custom_components/vesync/common.py | 8 +++ custom_components/vesync/const.py | 6 ++- custom_components/vesync/diagnostics.py | 10 ++-- custom_components/vesync/fan.py | 40 +++++++-------- custom_components/vesync/number.py | 62 ++++++++++++++++++++++- 6 files changed, 103 insertions(+), 29 deletions(-) diff --git a/custom_components/vesync/binary_sensor.py b/custom_components/vesync/binary_sensor.py index 9dcdc91..41dc5b2 100644 --- a/custom_components/vesync/binary_sensor.py +++ b/custom_components/vesync/binary_sensor.py @@ -41,8 +41,10 @@ def _setup_entities(devices, async_add_entities): entities = [] for dev in devices: if is_humidifier(dev.device_type): - entities.append(VeSyncOutOfWaterSensor(dev)) - entities.append(VeSyncWaterTankLiftedSensor(dev)) + entities.extend( + (VeSyncOutOfWaterSensor(dev), VeSyncWaterTankLiftedSensor(dev)) + ) + else: _LOGGER.warning( "%s - Unknown device type - %s", dev.device_name, dev.device_type diff --git a/custom_components/vesync/common.py b/custom_components/vesync/common.py index 5aa80f6..b20f60d 100644 --- a/custom_components/vesync/common.py +++ b/custom_components/vesync/common.py @@ -23,6 +23,11 @@ def is_humidifier(device_type: str) -> bool: return model_features(device_type)["module"].find("VeSyncHumid") > -1 +def is_air_purifier(device_type: str) -> bool: + """Return true if the device type is a an air purifier.""" + return model_features(device_type)["module"].find("VeSyncAirBypass") > -1 + + async def async_process_devices(hass, manager): """Assign devices to proper component.""" devices = { @@ -40,6 +45,7 @@ async def async_process_devices(hass, manager): if manager.fans: for fan in manager.fans: # VeSync classifies humidifiers as fans + _LOGGER.debug("Found a fan: %s", fan.__dict__) if is_humidifier(fan.device_type): devices[VS_HUMIDIFIERS].append(fan) devices[VS_NUMBERS].append(fan) # for night light and mist level @@ -51,6 +57,8 @@ async def async_process_devices(hass, manager): if fan.night_light: devices[VS_LIGHTS].append(fan) # for night light else: + if hasattr(fan, "config_dict"): + devices[VS_NUMBERS].append(fan) devices[VS_FANS].append(fan) _LOGGER.info("%d VeSync fans found", len(manager.fans)) diff --git a/custom_components/vesync/const.py b/custom_components/vesync/const.py index 043d5a9..fd9a8ce 100644 --- a/custom_components/vesync/const.py +++ b/custom_components/vesync/const.py @@ -17,10 +17,14 @@ VS_MODE_AUTO = "auto" VS_MODE_MANUAL = "manual" DEV_TYPE_TO_HA = { - "LV-PUR131S": "fan", "Core200S": "fan", "Core300S": "fan", "Core400S": "fan", + "LAP-C201S-AUSR": "fan", + "LAP-C202S-WUSR": "fan", + "LAP-C401S-WUSR": "fan", + "LAP-C601S-WUS": "fan", + "LV-PUR131S": "fan", "Classic300S": "humidifier", "ESD16": "walldimmer", "ESWD16": "walldimmer", diff --git a/custom_components/vesync/diagnostics.py b/custom_components/vesync/diagnostics.py index 186fc2f..e89d894 100644 --- a/custom_components/vesync/diagnostics.py +++ b/custom_components/vesync/diagnostics.py @@ -9,6 +9,10 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN +def _if_has_attr_else_none(obj, attr): + return getattr(obj, attr) if hasattr(obj, attr) else None + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: @@ -19,9 +23,9 @@ async def async_get_config_entry_diagnostics( for d in data["manager"]._dev_list[type]: devices[type].append( { - "device": d.config_dict or {}, - "config": d.config or {}, - "details": d.details or {}, + "config_dict": _if_has_attr_else_none(d, "config_dict") or {}, + "config": _if_has_attr_else_none(d, "config") or {}, + "details": _if_has_attr_else_none(d, "details") or {}, } ) return devices diff --git a/custom_components/vesync/fan.py b/custom_components/vesync/fan.py index 43dd688..1915257 100644 --- a/custom_components/vesync/fan.py +++ b/custom_components/vesync/fan.py @@ -20,14 +20,6 @@ _LOGGER = logging.getLogger(__name__) FAN_MODE_AUTO = "auto" FAN_MODE_SLEEP = "sleep" -# Fixme add other models -PRESET_MODES = { - "Core200S": [FAN_MODE_SLEEP], - "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], -} -SPEED_RANGE = (1, 3) # off is not included async def async_setup_entry( @@ -56,11 +48,12 @@ def _setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" entities = [] for dev in devices: + _LOGGER.debug("Adding device %s %s", dev.device_name, dev.device_type) if DEV_TYPE_TO_HA.get(dev.device_type) == "fan": entities.append(VeSyncFanHA(dev)) else: _LOGGER.warning( - "%s - Unknown device type - %s", dev.device_name, dev.device_type + "Unknown device type %s %s", dev.device_name, dev.device_type ) continue @@ -72,13 +65,25 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): def __init__(self, fan): """Initialize the VeSync fan device.""" + _LOGGER.debug("Initializing fan") super().__init__(fan) self.smartfan = fan + if hasattr(self.smartfan, "config_dict"): + self._speed_range = (1, max(self.smartfan.config_dict["levels"])) + self._attr_preset_modes = [ + mode + for mode in ["auto", "sleep"] + if mode in self.smartfan.config_dict["modes"] + ] + else: + self._speed_range = (1, 1) + self._attr_preset_modes = [] + self._attr_preset_modes = [FAN_MODE_AUTO, FAN_MODE_SLEEP] @property def supported_features(self): """Flag supported features.""" - return SUPPORT_SET_SPEED + return SUPPORT_SET_SPEED if self.speed_count > 1 else 0 @property def percentage(self): @@ -87,25 +92,18 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.mode == "manual" and (current_level := self.smartfan.fan_level) is not None ): - return ranged_value_to_percentage(SPEED_RANGE, current_level) + return ranged_value_to_percentage(self._speed_range, current_level) return None @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) - - @property - def preset_modes(self): - """Get the list of available preset modes.""" - return PRESET_MODES[self.device.device_type] + return int_states_in_range(self._speed_range) @property def preset_mode(self): """Get the current preset mode.""" - if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP): - return self.smartfan.mode - return None + return self.smartfan.mode @property def unique_info(self): @@ -151,7 +149,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.manual_mode() self.smartfan.change_fan_speed( - math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + math.ceil(percentage_to_ranged_value(self._speed_range, percentage)) ) self.schedule_update_ha_state() diff --git a/custom_components/vesync/number.py b/custom_components/vesync/number.py index 4711be4..4c71c12 100644 --- a/custom_components/vesync/number.py +++ b/custom_components/vesync/number.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .common import VeSyncBaseEntity, is_humidifier +from .common import VeSyncBaseEntity, is_air_purifier, is_humidifier from .const import DOMAIN, VS_DISCOVERY, VS_NUMBERS MAX_HUMIDITY = 80 @@ -50,7 +50,8 @@ def _setup_entities(devices, async_add_entities): VeSyncHumidifierTargetLevelHA(dev), ) ) - + elif is_air_purifier(dev.device_type): + entities.extend((VeSyncFanSpeedLevelHA(dev),)) else: _LOGGER.debug( "%s - Unknown device type - %s", dev.device_name, dev.device_type @@ -60,6 +61,63 @@ def _setup_entities(devices, async_add_entities): async_add_entities(entities, update_before_add=True) +class VeSyncFanNumberEntity(VeSyncBaseEntity, NumberEntity): + """Representation of a number for configuring a VeSync fan.""" + + def __init__(self, fan): + """Initialize the VeSync fan device.""" + super().__init__(fan) + self.smartfan = fan + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.CONFIG + + +class VeSyncFanSpeedLevelHA(VeSyncFanNumberEntity): + """Representation of the fan speed level of a VeSync fan.""" + + @property + def unique_id(self): + """Return the ID of this device.""" + return f"{super().unique_id}-fan-speed-level" + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} fan speed level" + + @property + def value(self): + """Return the fan speed level.""" + return self.device.speed + + @property + def min_value(self) -> float: + """Return the minimum fan speed level.""" + return self.device.config_dict["levels"][0] + + @property + def max_value(self) -> float: + """Return the maximum fan speed level.""" + return self.device.config_dict["levels"][-1] + + @property + def step(self) -> float: + """Return the steps for the fan speed level.""" + return 1.0 + + @property + def extra_state_attributes(self): + """Return the state attributes of the humidifier.""" + return {"fan speed levels": self.device.config_dict["levels"]} + + def set_value(self, value): + """Set the fan speed level.""" + self.device.change_fan_speed(int(value)) + + class VeSyncHumidifierNumberEntity(VeSyncBaseEntity, NumberEntity): """Representation of a number for configuring a VeSync humidifier."""