diff --git a/.coveragerc b/.coveragerc index 8f6db3b3935..a4a221db69a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,6 +1110,7 @@ omit = homeassistant/components/tradfri/base_class.py homeassistant/components/tradfri/config_flow.py homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/fan.py homeassistant/components/tradfri/light.py homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 1222e9b7792..a03f16a1f09 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -8,6 +8,8 @@ from typing import Any from pytradfri.command import Command from pytradfri.device import Device +from pytradfri.device.air_purifier import AirPurifier +from pytradfri.device.air_purifier_control import AirPurifierControl from pytradfri.device.blind import Blind from pytradfri.device.blind_control import BlindControl from pytradfri.device.light import Light @@ -58,10 +60,10 @@ class TradfriBaseClass(Entity): """Initialize a device.""" self._api = handle_error(api) self._device: Device = device - self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | None = ( + self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = ( None ) - self._device_data: Socket | Light | Blind | None = None + self._device_data: Socket | Light | Blind | AirPurifier | None = None self._gateway_id = gateway_id self._refresh(device) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 1f382548263..8efb1837ae4 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -2,6 +2,7 @@ from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import +ATTR_AUTO = "Auto" ATTR_DIMMER = "dimmer" ATTR_HUE = "hue" ATTR_SAT = "saturation" @@ -23,4 +24,4 @@ GROUPS = "tradfri_groups" KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION -PLATFORMS = ["cover", "light", "sensor", "switch"] +PLATFORMS = ["cover", "fan", "light", "sensor", "switch"] diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py new file mode 100644 index 00000000000..b2f8641addb --- /dev/null +++ b/homeassistant/components/tradfri/fan.py @@ -0,0 +1,175 @@ +"""Represent an air purifier.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, cast + +from pytradfri.command import Command + +from homeassistant.components.fan import ( + SUPPORT_PRESET_MODE, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .base_class import TradfriBaseDevice +from .const import ATTR_AUTO, CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Load Tradfri switches based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + tradfri_data = hass.data[DOMAIN][config_entry.entry_id] + api = tradfri_data[KEY_API] + devices = tradfri_data[DEVICES] + + purifiers = [dev for dev in devices if dev.has_air_purifier_control] + if purifiers: + async_add_entities( + TradfriAirPurifierFan(purifier, api, gateway_id) for purifier in purifiers + ) + + +def _from_percentage(percentage: int) -> int: + """Convert percent to a value that the Tradfri API understands.""" + if percentage < 20: + # The device cannot be set to speed 5 (10%), so we should turn off the device + # for any value below 20 + return 0 + + nearest_10: int = round(percentage / 10) * 10 # Round to nearest multiple of 10 + return round(nearest_10 / 100 * 50) + + +def _from_fan_speed(fan_speed: int) -> int: + """Convert the Tradfri API fan speed to a percentage value.""" + nearest_10: int = round(fan_speed / 10) * 10 # Round to nearest multiple of 10 + return round(nearest_10 / 50 * 100) + + +class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): + """The platform class required by Home Assistant.""" + + def __init__( + self, + device: Command, + api: Callable[[Command | list[Command]], Any], + gateway_id: str, + ) -> None: + """Initialize a switch.""" + super().__init__(device, api, gateway_id) + self._attr_unique_id = f"{gateway_id}-{device.id}" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_PRESET_MODE + SUPPORT_SET_SPEED + + @property + def speed_count(self) -> int: + """ + Return the number of speeds the fan supports. + + These are the steps: + 0 = Off + 10 = Min + 15 + 20 + 25 + 30 + 35 + 40 + 45 + 50 = Max + """ + return 10 + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + if not self._device_data: + return False + return cast(bool, self._device_data.mode) + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes.""" + return [ATTR_AUTO] + + @property + def percentage(self) -> int | None: + """Return the current speed percentage.""" + if not self._device_data: + return None + + if self._device_data.fan_speed: + return _from_fan_speed(self._device_data.fan_speed) + + return None + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + if not self._device_data: + return None + + if self._device_data.mode == ATTR_AUTO: + return ATTR_AUTO + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + if not self._device_control: + return + + if not preset_mode == ATTR_AUTO: + raise ValueError("Preset must be 'Auto'.") + await self._api(self._device_control.set_mode(1)) + + async def async_turn_on( + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if not self._device_control: + return + + if percentage is not None: + await self._api(self._device_control.set_mode(_from_percentage(percentage))) + return + + if preset_mode: + await self.async_set_preset_mode(preset_mode) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if not self._device_control: + return + + await self._api(self._device_control.set_mode(_from_percentage(percentage))) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the fan.""" + if not self._device_control: + return + await self._api(self._device_control.set_mode(0)) + + def _refresh(self, device: Command) -> None: + """Refresh the purifier data.""" + super()._refresh(device) + + # Caching of air purifier control and purifier object + self._device_control = device.air_purifier_control + self._device_data = device.air_purifier_control.air_purifiers[0] diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index f761aba5ddd..c6214773b97 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -34,6 +34,7 @@ async def async_setup_entry( and not dev.has_socket_control and not dev.has_blind_control and not dev.has_signal_repeater_control + and not dev.has_air_purifier_control ) if sensors: async_add_entities(TradfriSensor(sensor, api, gateway_id) for sensor in sensors) diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 370fba42fba..85e7f8dc037 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -139,6 +139,7 @@ def mock_light(test_features=None, test_state=None, light_number=0): has_socket_control=False, has_blind_control=False, has_signal_repeater_control=False, + has_air_purifier_control=False, ) _mock_light.name = f"tradfri_light_{light_number}" diff --git a/tests/components/tradfri/test_util.py b/tests/components/tradfri/test_util.py new file mode 100644 index 00000000000..3dbdf801f89 --- /dev/null +++ b/tests/components/tradfri/test_util.py @@ -0,0 +1,22 @@ +"""Tradfri utility function tests.""" + +from homeassistant.components.tradfri.fan import _from_fan_speed, _from_percentage + + +def test_from_fan_speed(): + """Test that we can convert fan speed to percentage value.""" + assert _from_fan_speed(41) == 80 + + +def test_from_percentage(): + """Test that we can convert percentage value to fan speed.""" + assert _from_percentage(84) == 40 + + +def test_from_percentage_limit(): + """ + Test that we can convert percentage value to fan speed. + + Handle special case of percent value being below 20. + """ + assert _from_percentage(10) == 0