From a71d58f4b2835d73b4e1a33b38931a0816362697 Mon Sep 17 00:00:00 2001 From: HuffYk Date: Mon, 3 Jun 2024 14:55:56 +0200 Subject: [PATCH] added support for everestair device --- custom_components/vesync/binary_sensor.py | 20 +++ custom_components/vesync/common.py | 2 +- custom_components/vesync/const.py | 3 +- custom_components/vesync/fan.py | 13 +- custom_components/vesync/light.py | 4 +- custom_components/vesync/number.py | 8 +- custom_components/vesync/sensor.py | 200 ++++++++++++++++++++-- 7 files changed, 227 insertions(+), 23 deletions(-) diff --git a/custom_components/vesync/binary_sensor.py b/custom_components/vesync/binary_sensor.py index 38e3eb4..4236bf1 100644 --- a/custom_components/vesync/binary_sensor.py +++ b/custom_components/vesync/binary_sensor.py @@ -58,6 +58,8 @@ def _setup_entities(devices, async_add_entities, coordinator): entities.append(VeSyncOutOfWaterSensor(dev, coordinator)) if has_feature(dev, "details", "water_tank_lifted"): entities.append(VeSyncWaterTankLiftedSensor(dev, coordinator)) + if has_feature(dev, "details", "filter_open_state"): + entities.append(VeSyncFilterOpenStateSensor(dev, coordinator)) async_add_entities(entities, update_before_add=True) @@ -148,3 +150,21 @@ class VeSyncWaterTankLiftedSensor(VeSyncBinarySensorEntity): def is_on(self) -> bool: """Return a value indicating whether the Humidifier's water tank is lifted.""" return self.smarthumidifier.details["water_tank_lifted"] + +class VeSyncFilterOpenStateSensor(VeSyncBinarySensorEntity): + """Filter Open Sensor.""" + + @property + def unique_id(self): + """Return unique ID for filter open state sensor on device.""" + return f"{super().unique_id}-filter-open-state" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} filter open state" + + @property + def is_on(self) -> bool: + """Return a value indicating whether the Humidifier's filter is open.""" + return self.smarthumidifier.details["filter_open_state"] diff --git a/custom_components/vesync/common.py b/custom_components/vesync/common.py index 02c4784..96ba099 100644 --- a/custom_components/vesync/common.py +++ b/custom_components/vesync/common.py @@ -146,7 +146,7 @@ class VeSyncBaseEntity(CoordinatorEntity, Entity): @property def base_name(self): """Return the name of the device.""" - return self.device.device_type + return self.device.device_name @property def name(self): diff --git a/custom_components/vesync/const.py b/custom_components/vesync/const.py index 96fe288..e8768c4 100644 --- a/custom_components/vesync/const.py +++ b/custom_components/vesync/const.py @@ -25,10 +25,11 @@ VS_MODE_AUTO = "auto" VS_MODE_HUMIDITY = "humidity" VS_MODE_MANUAL = "manual" VS_MODE_SLEEP = "sleep" +VS_MODE_TURBO = "turbo" VS_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"} -VS_FAN_TYPES = ["VeSyncAirBypass", "VeSyncAir131", "VeSyncVital"] +VS_FAN_TYPES = ["VeSyncAirBypass", "VeSyncAir131", "VeSyncVital", "VeSyncAirBaseV2"] VS_HUMIDIFIERS_TYPES = ["VeSyncHumid200300S", "VeSyncHumid200S", "VeSyncHumid1000S"] VS_AIRFRYER_TYPES = ["VeSyncAirFryer158"] diff --git a/custom_components/vesync/fan.py b/custom_components/vesync/fan.py index 25de507..a9b7f06 100644 --- a/custom_components/vesync/fan.py +++ b/custom_components/vesync/fan.py @@ -22,6 +22,7 @@ from .const import ( VS_MODE_AUTO, VS_MODE_MANUAL, VS_MODE_SLEEP, + VS_MODE_TURBO, VS_MODES, VS_TO_HA_ATTRIBUTES, ) @@ -69,15 +70,15 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan = fan self._speed_range = (1, 1) self._attr_preset_modes = [VS_MODE_MANUAL, VS_MODE_AUTO, VS_MODE_SLEEP] - if has_feature(self.smartfan, "config_dict", VS_LEVELS): - self._speed_range = (1, max(self.smartfan.config_dict[VS_LEVELS])) - if has_feature(self.smartfan, "config_dict", VS_MODES): + if has_feature(self.smartfan, "_config_dict", VS_LEVELS): + self._speed_range = (1, max(self.smartfan._config_dict[VS_LEVELS])) + if has_feature(self.smartfan, "_config_dict", VS_MODES): self._attr_preset_modes = [ VS_MODE_MANUAL, *[ mode - for mode in [VS_MODE_AUTO, VS_MODE_SLEEP] - if mode in self.smartfan.config_dict[VS_MODES] + for mode in [VS_MODE_AUTO, VS_MODE_SLEEP, VS_MODE_TURBO] + if mode in self.smartfan._config_dict[VS_MODES] ], ] if self.smartfan.device_type == "LV-PUR131S": @@ -161,6 +162,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.sleep_mode() elif preset_mode == VS_MODE_MANUAL: self.smartfan.manual_mode() + elif preset_mode == VS_MODE_TURBO: + self.smartfan.turbo_mode() self.schedule_update_ha_state() diff --git a/custom_components/vesync/light.py b/custom_components/vesync/light.py index 59ab5e5..19d7eb2 100644 --- a/custom_components/vesync/light.py +++ b/custom_components/vesync/light.py @@ -255,7 +255,7 @@ class VeSyncNightLightHA(VeSyncDimmableLightHA): def turn_on(self, **kwargs): """Turn the night light on.""" - if self.device.config_dict["module"] == "VeSyncAirBypass": + if self.device._config_dict["module"] == "VeSyncAirBypass": if ATTR_BRIGHTNESS in kwargs and kwargs[ATTR_BRIGHTNESS] < 255: self.device.set_night_light("dim") else: @@ -269,7 +269,7 @@ class VeSyncNightLightHA(VeSyncDimmableLightHA): def turn_off(self, **kwargs): """Turn the night light off.""" - if self.device.config_dict["module"] == "VeSyncAirBypass": + if self.device._config_dict["module"] == "VeSyncAirBypass": self.device.set_night_light("off") else: self.device.set_night_light_brightness(0) diff --git a/custom_components/vesync/number.py b/custom_components/vesync/number.py index 7a4cd53..3be0603 100644 --- a/custom_components/vesync/number.py +++ b/custom_components/vesync/number.py @@ -52,7 +52,7 @@ def _setup_entities(devices, async_add_entities, coordinator): entities.append(VeSyncHumidifierTargetLevelHA(dev, coordinator)) if has_feature(dev, "details", "warm_mist_level"): entities.append(VeSyncHumidifierWarmthLevelHA(dev, coordinator)) - if has_feature(dev, "config_dict", "levels"): + if has_feature(dev, "_config_dict", "levels"): entities.append(VeSyncFanSpeedLevelHA(dev, coordinator)) async_add_entities(entities, update_before_add=True) @@ -77,8 +77,8 @@ class VeSyncFanSpeedLevelHA(VeSyncNumberEntity): def __init__(self, device, coordinator) -> None: """Initialize the number entity.""" super().__init__(device, coordinator) - self._attr_native_min_value = device.config_dict["levels"][0] - self._attr_native_max_value = device.config_dict["levels"][-1] + self._attr_native_min_value = device._config_dict["levels"][0] + self._attr_native_max_value = device._config_dict["levels"][-1] self._attr_native_step = 1 @property @@ -99,7 +99,7 @@ class VeSyncFanSpeedLevelHA(VeSyncNumberEntity): @property def extra_state_attributes(self): """Return the state attributes of the humidifier.""" - return {"fan speed levels": self.device.config_dict["levels"]} + return {"fan speed levels": self.device._config_dict["levels"]} def set_native_value(self, value): """Set the fan speed level.""" diff --git a/custom_components/vesync/sensor.py b/custom_components/vesync/sensor.py index 4ce8198..bcd9b16 100644 --- a/custom_components/vesync/sensor.py +++ b/custom_components/vesync/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower +from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, DEGREE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -77,10 +77,18 @@ def _setup_entities(devices, async_add_entities, coordinator): entities.append(VeSyncHumiditySensor(dev, coordinator)) if has_feature(dev, "details", "air_quality"): entities.append(VeSyncAirQualitySensor(dev, coordinator)) + if has_feature(dev, "details", "aq_percent"): + entities.append(VeSyncAirQualityPercSensor(dev, coordinator)) if has_feature(dev, "details", "air_quality_value"): entities.append(VeSyncAirQualityValueSensor(dev, coordinator)) + if has_feature(dev, "details", "pm1"): + entities.append(VeSyncPM1Sensor(dev, coordinator)) + if has_feature(dev, "details", "pm10"): + entities.append(VeSyncPM10Sensor(dev, coordinator)) if has_feature(dev, "details", "filter_life"): entities.append(VeSyncFilterLifeSensor(dev, coordinator)) + if has_feature(dev, "details", "fan_rotate_angle"): + entities.append(VeSyncFanRotateAngleSensor(dev, coordinator)) async_add_entities(entities, update_before_add=True) @@ -238,14 +246,13 @@ class VeSyncHumidifierSensorEntity(VeSyncBaseEntity, SensorEntity): @property def entity_category(self): """Return the diagnostic entity category.""" - return EntityCategory.DIAGNOSTIC + return None class VeSyncAirQualitySensor(VeSyncHumidifierSensorEntity): """Representation of an air quality sensor.""" _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = " " def __init__(self, device, coordinator) -> None: """Initialize the VeSync device.""" @@ -262,12 +269,12 @@ class VeSyncAirQualitySensor(VeSyncHumidifierSensorEntity): @property def unique_id(self): """Return unique ID for air quality sensor on device.""" - return f"{super().unique_id}-air-quality" + return f"{super().unique_id}-air-quality-index" @property def name(self): """Return sensor name.""" - return f"{super().name} air quality" + return f"{super().name} air quality index" @property def native_value(self): @@ -284,13 +291,55 @@ class VeSyncAirQualitySensor(VeSyncHumidifierSensorEntity): _LOGGER.warning("No air quality index found in '%s'", self.name) return None +class VeSyncAirQualityPercSensor(VeSyncHumidifierSensorEntity): + """Representation of an air quality percentage sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_device_class = SensorDeviceClass.AQI + + def __init__(self, device, coordinator) -> None: + """Initialize the VeSync device.""" + super().__init__(device, coordinator) + self._numeric_quality = None + if self.native_value is not None: + self._numeric_quality = isinstance(self.native_value, (int, float)) + + @property + def unique_id(self): + """Return unique ID for air quality sensor on device.""" + return f"{super().unique_id}-air-quality" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} air quality" + + @property + def native_unit_of_measurement(self): + """Return the % unit of measurement.""" + return PERCENTAGE + + @property + def native_value(self): + """Return the air quality percentage.""" + if has_feature(self.smarthumidifier, "details", "aq_percent"): + quality = self.smarthumidifier.details["aq_percent"] + if isinstance(quality, (int, float)): + return quality + _LOGGER.warning( + "Got non numeric value for AQI sensor from 'aq_percent' for %s: %s", + self.name, + quality, + ) + _LOGGER.warning("No air quality percentage found in '%s'", self.name) + return None class VeSyncAirQualityValueSensor(VeSyncHumidifierSensorEntity): """Representation of an air quality sensor.""" _attr_state_class = SensorStateClass.MEASUREMENT - _attr_device_class = SensorDeviceClass.AQI - _attr_native_unit_of_measurement = " " + _attr_device_class = SensorDeviceClass.PM25 + _attr_native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER def __init__(self, device, coordinator) -> None: """Initialize the VeSync device.""" @@ -299,12 +348,12 @@ class VeSyncAirQualityValueSensor(VeSyncHumidifierSensorEntity): @property def unique_id(self): """Return unique ID for air quality sensor on device.""" - return f"{super().unique_id}-air-quality-value" + return f"{super().unique_id}-pm25" @property def name(self): """Return sensor name.""" - return f"{super().name} air quality value" + return f"{super().name} PM2.5" @property def native_value(self): @@ -321,14 +370,90 @@ class VeSyncAirQualityValueSensor(VeSyncHumidifierSensorEntity): _LOGGER.warning("No air quality value found in '%s'", self.name) return None +class VeSyncPM1Sensor(VeSyncHumidifierSensorEntity): + """Representation of a PM1 sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_device_class = SensorDeviceClass.PM1 + _attr_native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + + def __init__(self, device, coordinator) -> None: + """Initialize the VeSync device.""" + super().__init__(device, coordinator) + + @property + def unique_id(self): + """Return unique ID for PM1 sensor on device.""" + return f"{super().unique_id}-pm1" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} PM1" + + @property + def native_value(self): + """Return the PM1""" + if has_feature(self.smarthumidifier, "details", "pm1"): + quality_value = self.smarthumidifier.details["pm1"] + if isinstance(quality_value, (int, float)): + return quality_value + _LOGGER.warning( + "Got non numeric value for PM1 sensor from 'pm1' for %s: %s", + self.name, + quality_value, + ) + _LOGGER.warning("No PM1 value found in '%s'", self.name) + return None + +class VeSyncPM10Sensor(VeSyncHumidifierSensorEntity): + """Representation of a PM10 sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_device_class = SensorDeviceClass.PM10 + _attr_native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + + def __init__(self, device, coordinator) -> None: + """Initialize the VeSync device.""" + super().__init__(device, coordinator) + + @property + def unique_id(self): + """Return unique ID for PM10 sensor on device.""" + return f"{super().unique_id}-pm10" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} PM10" + + @property + def native_value(self): + """Return the PM10""" + if has_feature(self.smarthumidifier, "details", "pm10"): + quality_value = self.smarthumidifier.details["pm10"] + if isinstance(quality_value, (int, float)): + return quality_value + _LOGGER.warning( + "Got non numeric value for PM10 sensor from 'pm10' for %s: %s", + self.name, + quality_value, + ) + _LOGGER.warning("No PM10 value found in '%s'", self.name) + return None class VeSyncFilterLifeSensor(VeSyncHumidifierSensorEntity): """Representation of a filter life sensor.""" def __init__(self, plug, coordinator) -> None: - """Initialize the VeSync outlet device.""" + """Initialize the VeSync device.""" super().__init__(plug, coordinator) + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.DIAGNOSTIC + @property def unique_id(self): """Return unique ID for filter life sensor on device.""" @@ -372,6 +497,61 @@ class VeSyncFilterLifeSensor(VeSyncHumidifierSensorEntity): else {} ) + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:air-filter" + +class VeSyncFanRotateAngleSensor(VeSyncHumidifierSensorEntity): + """Representation of a fan rotate angle sensor.""" + + def __init__(self, plug, coordinator) -> None: + """Initialize the VeSync device.""" + super().__init__(plug, coordinator) + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.DIAGNOSTIC + + @property + def unique_id(self): + """Return unique ID for filter life sensor on device.""" + return f"{super().unique_id}-fan-rotate-angle" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} fan rotate angle" + + @property + def device_class(self): + """Return the fan rotate angle device class.""" + return None + + @property + def native_value(self): + """Return the fan rotate angle index.""" + return ( + self.smarthumidifier.fan_rotate_angle + if hasattr(self.smarthumidifier, "fan_rotate_angle") + else self.smarthumidifier.details["fan_rotate_angle"] + ) + + @property + def native_unit_of_measurement(self): + """Return the % unit of measurement.""" + return DEGREE + + @property + def state_class(self): + """Return the measurement state class.""" + return SensorStateClass.MEASUREMENT + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:rotate-3d-variant" class VeSyncHumiditySensor(VeSyncHumidifierSensorEntity): """Representation of current humidity for a VeSync humidifier."""