Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add more i35 sensors and switches. #142

Merged
merged 11 commits into from
Nov 28, 2024
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: 51 additions & 25 deletions custom_components/ha_blueair/blueair_aws_data_update_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
"""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

_LOGGER = logging.getLogger(__name__)

_FILTER_EXPIRED_THRESHOLD = 95
rainwoodman marked this conversation as resolved.
Show resolved Hide resolved


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 +31,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 +66,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:
dahlb marked this conversation as resolved.
Show resolved Hide resolved
return 64
else:
return 100

@property
def is_on(self) -> False:
"""Return the current fan state."""
Expand Down Expand Up @@ -118,44 +140,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()
dahlb marked this conversation as resolved.
Show resolved Hide resolved

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

DEFAULT_FAN_SPEED_PERCENTAGE = 50

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.10.1"],
rainwoodman marked this conversation as resolved.
Show resolved Hide resolved
"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()