diff --git a/custom_components/ha_blueair/binary_sensor.py b/custom_components/ha_blueair/binary_sensor.py index 6e18380..8e10e8f 100644 --- a/custom_components/ha_blueair/binary_sensor.py +++ b/custom_components/ha_blueair/binary_sensor.py @@ -8,6 +8,7 @@ from .const import DOMAIN, DATA_DEVICES, DATA_AWS_DEVICES from .blueair_data_update_coordinator import BlueairDataUpdateCoordinator +from .blueair_aws_data_update_coordinator import BlueairAwsDataUpdateCoordinator from .entity import BlueairEntity @@ -25,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) async_add_entities(entities) - aws_devices: list[BlueairDataUpdateCoordinator] = hass.data[DOMAIN][ + aws_devices: list[BlueairAwsDataUpdateCoordinator] = hass.data[DOMAIN][ DATA_AWS_DEVICES ] entities = [] @@ -34,6 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): [ BlueairFilterExpiredSensor(device), BlueairOnlineSensor(device), + BlueairWaterShortageSensor(device), ] ) async_add_entities(entities) @@ -55,11 +57,11 @@ class BlueairFilterExpiredSensor(BlueairEntity, BinarySensorEntity): _attr_icon = "mdi:air-filter" def __init__(self, device): + """Initialize the temperature sensor.""" self.entity_description = EntityDescription( key=f"#{device.blueair_api_device.uuid}-filter-expired", device_class=BinarySensorDeviceClass.PROBLEM, ) - """Initialize the temperature sensor.""" super().__init__("Filter Expiration", device) @property @@ -72,11 +74,11 @@ class BlueairOnlineSensor(BlueairEntity, BinarySensorEntity): _attr_icon = "mdi:wifi-check" def __init__(self, device): + """Initialize the temperature sensor.""" self.entity_description = EntityDescription( key=f"#{device.blueair_api_device.uuid}-online", device_class=BinarySensorDeviceClass.CONNECTIVITY, ) - """Initialize the temperature sensor.""" super().__init__("Online", device) @property @@ -90,3 +92,18 @@ def icon(self) -> str | None: return self._attr_icon else: return "mdi:wifi-strength-outline" + +class BlueairWaterShortageSensor(BlueairEntity, BinarySensorEntity): + _attr_icon = "mdi:water-alert-outline" + + def __init__(self, device): + self.entity_description = EntityDescription( + key=f"#{device.blueair_api_device.uuid}-water-shortage", + device_class=BinarySensorDeviceClass.PROBLEM, + ) + super().__init__("Water Shortage", device) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self._device.water_shortage diff --git a/custom_components/ha_blueair/blueair_aws_data_update_coordinator.py b/custom_components/ha_blueair/blueair_aws_data_update_coordinator.py index 5e22fe3..e2fba9b 100644 --- a/custom_components/ha_blueair/blueair_aws_data_update_coordinator.py +++ b/custom_components/ha_blueair/blueair_aws_data_update_coordinator.py @@ -1,18 +1,24 @@ """Blueair device object.""" import logging from datetime import timedelta - +import enum from blueair_api import DeviceAws as BlueAirApiDeviceAws -from asyncio import sleep from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.debounce import Debouncer -from .const import DOMAIN +from .const import DOMAIN, FILTER_EXPIRED_THRESHOLD _LOGGER = logging.getLogger(__name__) +class ModelEnum(enum.StrEnum): + UNKNOWN = "Unknown" + HUMIDIFIER_I35 = "Blueair Humidifier i35" + PROTECT_7470I = "Blueair Protect 7470i" + + class BlueairAwsDataUpdateCoordinator(DataUpdateCoordinator): """Blueair device object.""" @@ -23,12 +29,14 @@ def __init__( self.hass: HomeAssistant = hass self.blueair_api_device: BlueAirApiDeviceAws = blueair_api_device self._manufacturer: str = "BlueAir" - super().__init__( hass, _LOGGER, name=f"{DOMAIN}-{self.blueair_api_device.uuid}", update_interval=timedelta(minutes=10), + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=5.0, immediate=False, + ), ) async def _async_update_data(self): @@ -56,14 +64,26 @@ def manufacturer(self) -> str: return self._manufacturer @property - def model(self) -> str: - return "protect?" + def model(self) -> ModelEnum: + if self.blueair_api_device.sku == "111633": + return ModelEnum.HUMIDIFIER_I35 + if self.blueair_api_device.sku == "105826": + return ModelEnum.PROTECT_7470I + return ModelEnum.UNKNOWN @property def fan_speed(self) -> int: """Return the current fan speed.""" return self.blueair_api_device.fan_speed + @property + def speed_count(self) -> int: + """Return the max fan speed.""" + if self.model == ModelEnum.HUMIDIFIER_I35: + return 64 + else: + return 100 + @property def is_on(self) -> False: """Return the current fan state.""" @@ -118,44 +138,48 @@ def online(self) -> bool: def fan_auto_mode(self) -> bool: return self.blueair_api_device.fan_auto_mode + @property + def wick_dry_mode(self) -> bool: + return self.blueair_api_device.wick_dry_mode + + @property + def water_shortage(self) -> bool: + return self.blueair_api_device.water_shortage + @property def filter_expired(self) -> bool: """Return the current filter status.""" - return (self.blueair_api_device.filter_usage is not None - and self.blueair_api_device.filter_usage >= 95) + if self.blueair_api_device.filter_usage is not None: + return (self.blueair_api_device.filter_usage >= + FILTER_EXPIRED_THRESHOLD) + if self.blueair_api_device.wick_usage is not None: + return (self.blueair_api_device.wick_usage >= + FILTER_EXPIRED_THRESHOLD) async def set_fan_speed(self, new_speed) -> None: - self.blueair_api_device.fan_speed = new_speed await self.blueair_api_device.set_fan_speed(new_speed) - await sleep(5) - await self.async_refresh() + await self.async_request_refresh() async def set_running(self, running) -> None: - self.blueair_api_device.running = running await self.blueair_api_device.set_running(running) - await sleep(5) - await self.async_refresh() + await self.async_request_refresh() async def set_brightness(self, brightness) -> None: - self.blueair_api_device.brightness = brightness await self.blueair_api_device.set_brightness(brightness) - await sleep(5) - await self.async_refresh() + await self.async_request_refresh() async def set_child_lock(self, locked) -> None: - self.blueair_api_device.child_lock = locked await self.blueair_api_device.set_child_lock(locked) - await sleep(5) - await self.async_refresh() + await self.async_request_refresh() async def set_night_mode(self, mode) -> None: - self.blueair_api_device.night_mode = mode await self.blueair_api_device.set_night_mode(mode) - await sleep(5) - await self.async_refresh() + await self.async_request_refresh() async def set_fan_auto_mode(self, value) -> None: - self.blueair_api_device.fan_auto_mode = value await self.blueair_api_device.set_fan_auto_mode(value) - await sleep(5) - await self.async_refresh() + await self.async_request_refresh() + + async def set_wick_dry_mode(self, value) -> None: + await self.blueair_api_device.set_wick_dry_mode(value) + await self.async_request_refresh() diff --git a/custom_components/ha_blueair/const.py b/custom_components/ha_blueair/const.py index 248c4f5..421a34e 100644 --- a/custom_components/ha_blueair/const.py +++ b/custom_components/ha_blueair/const.py @@ -13,3 +13,8 @@ REGION_EU = "eu" REGION_USA = "us" REGIONS = [REGION_USA, REGION_EU] + +DEFAULT_FAN_SPEED_PERCENTAGE = 50 +FILTER_EXPIRED_THRESHOLD = 95 + + diff --git a/custom_components/ha_blueair/entity.py b/custom_components/ha_blueair/entity.py index 581e365..49c12e8 100644 --- a/custom_components/ha_blueair/entity.py +++ b/custom_components/ha_blueair/entity.py @@ -6,6 +6,7 @@ ) from .const import DOMAIN +from .blueair_data_update_coordinator import BlueairDataUpdateCoordinator from .blueair_aws_data_update_coordinator import BlueairAwsDataUpdateCoordinator @@ -18,7 +19,7 @@ class BlueairEntity(CoordinatorEntity): def __init__( self, entity_type: str, - device: BlueairAwsDataUpdateCoordinator, + device: BlueairAwsDataUpdateCoordinator | BlueairDataUpdateCoordinator, **kwargs, ) -> None: super().__init__(device) @@ -40,6 +41,9 @@ def device_info(self) -> DeviceInfo: async def async_update(self): """Update Blueair entity.""" + if not self.enabled: + return + await self._device.async_request_refresh() self._attr_available = self._device.blueair_api_device.wifi_working diff --git a/custom_components/ha_blueair/fan.py b/custom_components/ha_blueair/fan.py index f97773a..7528b9c 100644 --- a/custom_components/ha_blueair/fan.py +++ b/custom_components/ha_blueair/fan.py @@ -6,7 +6,7 @@ FanEntityFeature, ) -from .const import DOMAIN, DATA_DEVICES, DATA_AWS_DEVICES +from .const import DOMAIN, DATA_DEVICES, DATA_AWS_DEVICES, DEFAULT_FAN_SPEED_PERCENTAGE from .blueair_data_update_coordinator import BlueairDataUpdateCoordinator from .blueair_aws_data_update_coordinator import BlueairAwsDataUpdateCoordinator from .entity import BlueairEntity @@ -107,10 +107,10 @@ def is_on(self) -> int: @property def percentage(self) -> int: """Return the current speed percentage.""" - return self._device.fan_speed + return int(self._device.fan_speed / self._device.speed_count * 100) async def async_set_percentage(self, percentage: int) -> None: - await self._device.set_fan_speed(percentage) + await self._device.set_fan_speed(int(percentage / 100 * self._device.speed_count)) self.async_write_ha_state() async def async_turn_off(self, **kwargs: any) -> None: @@ -125,10 +125,17 @@ async def async_turn_on( ) -> None: await self._device.set_running(True) self.async_write_ha_state() + if percentage is None: + # FIXME: i35 (and probably others) do not remember the + # last fan speed and always set the speed to 0. I don't know + # where to store the last fan speed such that it persists across + # HA reboots. Thus we set the default turn_on fan speed to 50% + # to make sure the fan actually spins at all. + percentage = DEFAULT_FAN_SPEED_PERCENTAGE if percentage is not None: await self.async_set_percentage(percentage=percentage) @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return 100 + return self._device.speed_count diff --git a/custom_components/ha_blueair/manifest.json b/custom_components/ha_blueair/manifest.json index dd1b434..fa74e6c 100644 --- a/custom_components/ha_blueair/manifest.json +++ b/custom_components/ha_blueair/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://github.com/dahlb/ha_blueair", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/dahlb/ha_blueair/issues", - "requirements": ["blueair-api==1.9.5"], + "requirements": ["blueair-api==1.11.0"], "version": "1.9.6" } diff --git a/custom_components/ha_blueair/sensor.py b/custom_components/ha_blueair/sensor.py index ba2de04..ec8cbb3 100644 --- a/custom_components/ha_blueair/sensor.py +++ b/custom_components/ha_blueair/sensor.py @@ -10,27 +10,33 @@ from .const import DOMAIN, DATA_AWS_DEVICES -from .blueair_data_update_coordinator import BlueairDataUpdateCoordinator +from .blueair_aws_data_update_coordinator import BlueairAwsDataUpdateCoordinator, ModelEnum from .entity import BlueairEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Blueair sensors from config entry.""" - aws_devices: list[BlueairDataUpdateCoordinator] = hass.data[DOMAIN][ + aws_devices: list[BlueairAwsDataUpdateCoordinator] = hass.data[DOMAIN][ DATA_AWS_DEVICES ] entities = [] + for device in aws_devices: - entities.extend( - [ + if device.model in (ModelEnum.UNKNOWN, ModelEnum.PROTECT_7470I): + entities.extend([ BlueairTemperatureSensor(device), BlueairHumiditySensor(device), BlueairVOCSensor(device), BlueairPM1Sensor(device), BlueairPM10Sensor(device), BlueairPM25Sensor(device), - ] - ) + ]) + elif device.model == ModelEnum.HUMIDIFIER_I35: + entities.extend([ + BlueairTemperatureSensor(device), + BlueairHumiditySensor(device), + ]) + async_add_entities(entities) diff --git a/custom_components/ha_blueair/switch.py b/custom_components/ha_blueair/switch.py index 075058f..57c8b69 100644 --- a/custom_components/ha_blueair/switch.py +++ b/custom_components/ha_blueair/switch.py @@ -22,6 +22,7 @@ async def async_setup_entry(hass, _config_entry, async_add_entities): BlueairChildLockSwitchEntity(device), BlueairAutoFanModeSwitchEntity(device), BlueairNightModeSwitchEntity(device), + BlueairWickDryModeSwitchEntity(device), ] ) async_add_entities(entities) @@ -87,3 +88,26 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): await self._device.set_night_mode(False) self.async_write_ha_state() + +class BlueairWickDryModeSwitchEntity(BlueairEntity, SwitchEntity): + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__(self, device): + super().__init__("Wick Dry Mode", device) + + @property + def is_on(self) -> int | None: + return self._device.wick_dry_mode + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._device.wick_dry_mode is not None + + async def async_turn_on(self, **kwargs): + await self._device.set_wick_dry_mode(True) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + await self._device.set_wick_dry_mode(False) + self.async_write_ha_state()