Skip to content

Commit

Permalink
feat: Add more i35 sensors and switches. (#142)
Browse files Browse the repository at this point in the history
* add model name.

* Better i35 support.

* speed count = 64
* some of the sensors do not apply
* set a default perentage to 50% when turning on. (because device does not remember last speed)
* added a few sensors.
* added model name look up for AWS devices i35 and 7470i
* fixed a few mis-typed type annotations.

* Remove unused property mutation.

the setters will and shall internally update these 'local cached state' properties
anyway.

* comment about default fan speed.

* bump version of blueair api.

* fix type annotation error.

* Use the debouncer, recommended by ha documentation

* Set Deboncer to use the 5 second delay.

Also avoid the first, immediate action because the cloud side state
is not updated till after 5 seconds.

* Move constant to const.py.

* remove unused sleep import.

* Move const and bump blueair_api version.
  • Loading branch information
rainwoodman authored Nov 28, 2024
1 parent dd30c24 commit a814c50
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 41 deletions.
23 changes: 20 additions & 3 deletions custom_components/ha_blueair/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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 = []
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
76 changes: 50 additions & 26 deletions custom_components/ha_blueair/blueair_aws_data_update_coordinator.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand All @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
5 changes: 5 additions & 0 deletions custom_components/ha_blueair/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
REGION_EU = "eu"
REGION_USA = "us"
REGIONS = [REGION_USA, REGION_EU]

DEFAULT_FAN_SPEED_PERCENTAGE = 50
FILTER_EXPIRED_THRESHOLD = 95


6 changes: 5 additions & 1 deletion custom_components/ha_blueair/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
)

from .const import DOMAIN
from .blueair_data_update_coordinator import BlueairDataUpdateCoordinator
from .blueair_aws_data_update_coordinator import BlueairAwsDataUpdateCoordinator


Expand All @@ -18,7 +19,7 @@ class BlueairEntity(CoordinatorEntity):
def __init__(
self,
entity_type: str,
device: BlueairAwsDataUpdateCoordinator,
device: BlueairAwsDataUpdateCoordinator | BlueairDataUpdateCoordinator,
**kwargs,
) -> None:
super().__init__(device)
Expand All @@ -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

Expand Down
15 changes: 11 additions & 4 deletions custom_components/ha_blueair/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
2 changes: 1 addition & 1 deletion custom_components/ha_blueair/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
18 changes: 12 additions & 6 deletions custom_components/ha_blueair/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
24 changes: 24 additions & 0 deletions custom_components/ha_blueair/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()

0 comments on commit a814c50

Please sign in to comment.