470 lines
15 KiB
Python
470 lines
15 KiB
Python
"""Support for Dyson Pure Cool link fan."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
|
|
from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation
|
|
from libpurecool.dyson_pure_cool import DysonPureCool
|
|
from libpurecool.dyson_pure_cool_link import DysonPureCoolLink
|
|
from libpurecool.dyson_pure_state import DysonPureCoolState
|
|
from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity
|
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
|
from homeassistant.util.percentage import (
|
|
int_states_in_range,
|
|
percentage_to_ranged_value,
|
|
ranged_value_to_percentage,
|
|
)
|
|
|
|
from . import DYSON_DEVICES, DysonEntity
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_NIGHT_MODE = "night_mode"
|
|
ATTR_AUTO_MODE = "auto_mode"
|
|
ATTR_ANGLE_LOW = "angle_low"
|
|
ATTR_ANGLE_HIGH = "angle_high"
|
|
ATTR_FLOW_DIRECTION_FRONT = "flow_direction_front"
|
|
ATTR_TIMER = "timer"
|
|
ATTR_HEPA_FILTER = "hepa_filter"
|
|
ATTR_CARBON_FILTER = "carbon_filter"
|
|
ATTR_DYSON_SPEED = "dyson_speed"
|
|
ATTR_DYSON_SPEED_LIST = "dyson_speed_list"
|
|
|
|
DYSON_DOMAIN = "dyson"
|
|
DYSON_FAN_DEVICES = "dyson_fan_devices"
|
|
|
|
SERVICE_SET_NIGHT_MODE = "set_night_mode"
|
|
SERVICE_SET_AUTO_MODE = "set_auto_mode"
|
|
SERVICE_SET_ANGLE = "set_angle"
|
|
SERVICE_SET_FLOW_DIRECTION_FRONT = "set_flow_direction_front"
|
|
SERVICE_SET_TIMER = "set_timer"
|
|
SERVICE_SET_DYSON_SPEED = "set_speed"
|
|
|
|
SET_NIGHT_MODE_SCHEMA = {
|
|
vol.Required(ATTR_NIGHT_MODE): cv.boolean,
|
|
}
|
|
|
|
SET_AUTO_MODE_SCHEMA = {
|
|
vol.Required(ATTR_AUTO_MODE): cv.boolean,
|
|
}
|
|
|
|
SET_ANGLE_SCHEMA = {
|
|
vol.Required(ATTR_ANGLE_LOW): cv.positive_int,
|
|
vol.Required(ATTR_ANGLE_HIGH): cv.positive_int,
|
|
}
|
|
|
|
SET_FLOW_DIRECTION_FRONT_SCHEMA = {
|
|
vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean,
|
|
}
|
|
|
|
SET_TIMER_SCHEMA = {
|
|
vol.Required(ATTR_TIMER): cv.positive_int,
|
|
}
|
|
|
|
SET_DYSON_SPEED_SCHEMA = {
|
|
vol.Required(ATTR_DYSON_SPEED): cv.positive_int,
|
|
}
|
|
|
|
|
|
PRESET_MODE_AUTO = "auto"
|
|
PRESET_MODES = [PRESET_MODE_AUTO]
|
|
|
|
ORDERED_DYSON_SPEEDS = [
|
|
FanSpeed.FAN_SPEED_1,
|
|
FanSpeed.FAN_SPEED_2,
|
|
FanSpeed.FAN_SPEED_3,
|
|
FanSpeed.FAN_SPEED_4,
|
|
FanSpeed.FAN_SPEED_5,
|
|
FanSpeed.FAN_SPEED_6,
|
|
FanSpeed.FAN_SPEED_7,
|
|
FanSpeed.FAN_SPEED_8,
|
|
FanSpeed.FAN_SPEED_9,
|
|
FanSpeed.FAN_SPEED_10,
|
|
]
|
|
DYSON_SPEED_TO_INT_VALUE = {k: int(k.value) for k in ORDERED_DYSON_SPEEDS}
|
|
INT_VALUE_TO_DYSON_SPEED = {v: k for k, v in DYSON_SPEED_TO_INT_VALUE.items()}
|
|
|
|
SPEED_LIST_DYSON = list(DYSON_SPEED_TO_INT_VALUE.values())
|
|
|
|
SPEED_RANGE = (
|
|
SPEED_LIST_DYSON[0],
|
|
SPEED_LIST_DYSON[-1],
|
|
) # off is not included
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
|
"""Set up the Dyson fan components."""
|
|
|
|
if discovery_info is None:
|
|
return
|
|
|
|
_LOGGER.debug("Creating new Dyson fans")
|
|
if DYSON_FAN_DEVICES not in hass.data:
|
|
hass.data[DYSON_FAN_DEVICES] = []
|
|
|
|
# Get Dyson Devices from parent component
|
|
has_purecool_devices = False
|
|
device_serials = [device.serial for device in hass.data[DYSON_FAN_DEVICES]]
|
|
for device in hass.data[DYSON_DEVICES]:
|
|
if device.serial not in device_serials:
|
|
if isinstance(device, DysonPureCool):
|
|
has_purecool_devices = True
|
|
dyson_entity = DysonPureCoolEntity(device)
|
|
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)
|
|
elif isinstance(device, DysonPureCoolLink):
|
|
dyson_entity = DysonPureCoolLinkEntity(device)
|
|
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)
|
|
|
|
async_add_entities(hass.data[DYSON_FAN_DEVICES])
|
|
|
|
# Register custom services
|
|
platform = entity_platform.async_get_current_platform()
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_NIGHT_MODE, SET_NIGHT_MODE_SCHEMA, "set_night_mode"
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_AUTO_MODE, SET_AUTO_MODE_SCHEMA, "set_auto_mode"
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_DYSON_SPEED, SET_DYSON_SPEED_SCHEMA, "service_set_dyson_speed"
|
|
)
|
|
if has_purecool_devices:
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_ANGLE, SET_ANGLE_SCHEMA, "set_angle"
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_FLOW_DIRECTION_FRONT,
|
|
SET_FLOW_DIRECTION_FRONT_SCHEMA,
|
|
"set_flow_direction_front",
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SET_TIMER, SET_TIMER_SCHEMA, "set_timer"
|
|
)
|
|
|
|
|
|
class DysonFanEntity(DysonEntity, FanEntity):
|
|
"""Representation of a Dyson fan."""
|
|
|
|
@property
|
|
def percentage(self):
|
|
"""Return the current speed percentage."""
|
|
if self.auto_mode:
|
|
return None
|
|
return ranged_value_to_percentage(SPEED_RANGE, int(self._device.state.speed))
|
|
|
|
@property
|
|
def speed_count(self) -> int:
|
|
"""Return the number of speeds the fan supports."""
|
|
return int_states_in_range(SPEED_RANGE)
|
|
|
|
@property
|
|
def preset_modes(self):
|
|
"""Return the available preset modes."""
|
|
return PRESET_MODES
|
|
|
|
@property
|
|
def preset_mode(self):
|
|
"""Return the current preset mode."""
|
|
if self.auto_mode:
|
|
return PRESET_MODE_AUTO
|
|
return None
|
|
|
|
@property
|
|
def dyson_speed(self):
|
|
"""Return the current speed."""
|
|
if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value:
|
|
return self._device.state.speed
|
|
return int(self._device.state.speed)
|
|
|
|
@property
|
|
def dyson_speed_list(self) -> list:
|
|
"""Get the list of available dyson speeds."""
|
|
return SPEED_LIST_DYSON
|
|
|
|
@property
|
|
def night_mode(self):
|
|
"""Return Night mode."""
|
|
return self._device.state.night_mode == "ON"
|
|
|
|
@property
|
|
def auto_mode(self):
|
|
"""Return auto mode."""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Flag supported features."""
|
|
return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict:
|
|
"""Return optional state attributes."""
|
|
return {
|
|
ATTR_NIGHT_MODE: self.night_mode,
|
|
ATTR_AUTO_MODE: self.auto_mode,
|
|
ATTR_DYSON_SPEED: self.dyson_speed,
|
|
ATTR_DYSON_SPEED_LIST: self.dyson_speed_list,
|
|
}
|
|
|
|
def set_auto_mode(self, auto_mode: bool) -> None:
|
|
"""Set auto mode."""
|
|
raise NotImplementedError
|
|
|
|
def set_percentage(self, percentage: int) -> None:
|
|
"""Set the speed percentage of the fan."""
|
|
if percentage == 0:
|
|
self.turn_off()
|
|
return
|
|
dyson_speed = INT_VALUE_TO_DYSON_SPEED[
|
|
math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
|
]
|
|
self.set_dyson_speed(dyson_speed)
|
|
|
|
def set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Set a preset mode on the fan."""
|
|
self._valid_preset_mode_or_raise(preset_mode)
|
|
# There currently is only one
|
|
self.set_auto_mode(True)
|
|
|
|
def set_dyson_speed(self, speed: FanSpeed) -> None:
|
|
"""Set the exact speed of the fan."""
|
|
raise NotImplementedError
|
|
|
|
def service_set_dyson_speed(self, dyson_speed: int) -> None:
|
|
"""Handle the service to set dyson speed."""
|
|
if dyson_speed not in SPEED_LIST_DYSON:
|
|
raise ValueError(f'"{dyson_speed}" is not a valid Dyson speed')
|
|
_LOGGER.debug("Set exact speed to %s", dyson_speed)
|
|
speed = FanSpeed(f"{int(dyson_speed):04d}")
|
|
self.set_dyson_speed(speed)
|
|
|
|
def turn_on(
|
|
self,
|
|
speed: str | None = None,
|
|
percentage: int | None = None,
|
|
preset_mode: str | None = None,
|
|
**kwargs,
|
|
) -> None:
|
|
"""Turn on the fan."""
|
|
_LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage)
|
|
if preset_mode:
|
|
self.set_preset_mode(preset_mode)
|
|
elif percentage is None:
|
|
# percentage not set, just turn on
|
|
self._device.set_configuration(fan_mode=FanMode.FAN)
|
|
else:
|
|
self.set_percentage(percentage)
|
|
|
|
|
|
class DysonPureCoolLinkEntity(DysonFanEntity):
|
|
"""Representation of a Dyson fan."""
|
|
|
|
def __init__(self, device):
|
|
"""Initialize the fan."""
|
|
super().__init__(device, DysonPureCoolState)
|
|
|
|
def turn_off(self, **kwargs) -> None:
|
|
"""Turn off the fan."""
|
|
_LOGGER.debug("Turn off fan %s", self.name)
|
|
self._device.set_configuration(fan_mode=FanMode.OFF)
|
|
|
|
def set_dyson_speed(self, speed: FanSpeed) -> None:
|
|
"""Set the exact speed of the fan."""
|
|
self._device.set_configuration(fan_mode=FanMode.FAN, fan_speed=speed)
|
|
|
|
def oscillate(self, oscillating: bool) -> None:
|
|
"""Turn on/off oscillating."""
|
|
_LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name)
|
|
|
|
if oscillating:
|
|
self._device.set_configuration(oscillation=Oscillation.OSCILLATION_ON)
|
|
else:
|
|
self._device.set_configuration(oscillation=Oscillation.OSCILLATION_OFF)
|
|
|
|
@property
|
|
def oscillating(self):
|
|
"""Return the oscillation state."""
|
|
return self._device.state.oscillation == "ON"
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if the entity is on."""
|
|
return self._device.state.fan_mode in ["FAN", "AUTO"]
|
|
|
|
def set_night_mode(self, night_mode: bool) -> None:
|
|
"""Turn fan in night mode."""
|
|
_LOGGER.debug("Set %s night mode %s", self.name, night_mode)
|
|
if night_mode:
|
|
self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON)
|
|
else:
|
|
self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF)
|
|
|
|
@property
|
|
def auto_mode(self):
|
|
"""Return auto mode."""
|
|
return self._device.state.fan_mode == "AUTO"
|
|
|
|
def set_auto_mode(self, auto_mode: bool) -> None:
|
|
"""Turn fan in auto mode."""
|
|
_LOGGER.debug("Set %s auto mode %s", self.name, auto_mode)
|
|
if auto_mode:
|
|
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
|
else:
|
|
self._device.set_configuration(fan_mode=FanMode.FAN)
|
|
|
|
|
|
class DysonPureCoolEntity(DysonFanEntity):
|
|
"""Representation of a Dyson Purecool (TP04/DP04) fan."""
|
|
|
|
def __init__(self, device):
|
|
"""Initialize the fan."""
|
|
super().__init__(device, DysonPureCoolV2State)
|
|
|
|
def turn_on(
|
|
self,
|
|
speed: str | None = None,
|
|
percentage: int | None = None,
|
|
preset_mode: str | None = None,
|
|
**kwargs,
|
|
) -> None:
|
|
"""Turn on the fan."""
|
|
_LOGGER.debug("Turn on fan %s with percentage %s", self.name, percentage)
|
|
if preset_mode:
|
|
self.set_preset_mode(preset_mode)
|
|
elif percentage is None:
|
|
# percentage not set, just turn on
|
|
self._device.turn_on()
|
|
else:
|
|
self.set_percentage(percentage)
|
|
|
|
def turn_off(self, **kwargs):
|
|
"""Turn off the fan."""
|
|
_LOGGER.debug("Turn off fan %s", self.name)
|
|
self._device.turn_off()
|
|
|
|
def set_dyson_speed(self, speed: FanSpeed) -> None:
|
|
"""Set the exact speed of the purecool fan."""
|
|
self._device.set_fan_speed(speed)
|
|
|
|
def oscillate(self, oscillating: bool) -> None:
|
|
"""Turn on/off oscillating."""
|
|
_LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name)
|
|
|
|
if oscillating:
|
|
self._device.enable_oscillation()
|
|
else:
|
|
self._device.disable_oscillation()
|
|
|
|
def set_night_mode(self, night_mode: bool) -> None:
|
|
"""Turn on/off night mode."""
|
|
_LOGGER.debug("Turn night mode %s for device %s", night_mode, self.name)
|
|
|
|
if night_mode:
|
|
self._device.enable_night_mode()
|
|
else:
|
|
self._device.disable_night_mode()
|
|
|
|
def set_auto_mode(self, auto_mode: bool) -> None:
|
|
"""Turn auto mode on/off."""
|
|
_LOGGER.debug("Turn auto mode %s for device %s", auto_mode, self.name)
|
|
if auto_mode:
|
|
self._device.enable_auto_mode()
|
|
else:
|
|
self._device.disable_auto_mode()
|
|
|
|
def set_angle(self, angle_low: int, angle_high: int) -> None:
|
|
"""Set device angle."""
|
|
_LOGGER.debug(
|
|
"set low %s and high angle %s for device %s",
|
|
angle_low,
|
|
angle_high,
|
|
self.name,
|
|
)
|
|
self._device.enable_oscillation(angle_low, angle_high)
|
|
|
|
def set_flow_direction_front(self, flow_direction_front: bool) -> None:
|
|
"""Set frontal airflow direction."""
|
|
_LOGGER.debug(
|
|
"Set frontal flow direction to %s for device %s",
|
|
flow_direction_front,
|
|
self.name,
|
|
)
|
|
|
|
if flow_direction_front:
|
|
self._device.enable_frontal_direction()
|
|
else:
|
|
self._device.disable_frontal_direction()
|
|
|
|
def set_timer(self, timer) -> None:
|
|
"""Set timer."""
|
|
_LOGGER.debug("Set timer to %s for device %s", timer, self.name)
|
|
|
|
if timer == 0:
|
|
self._device.disable_sleep_timer()
|
|
else:
|
|
self._device.enable_sleep_timer(timer)
|
|
|
|
@property
|
|
def oscillating(self):
|
|
"""Return the oscillation state."""
|
|
return self._device.state and self._device.state.oscillation == "OION"
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if the entity is on."""
|
|
return self._device.state.fan_power == "ON"
|
|
|
|
@property
|
|
def auto_mode(self):
|
|
"""Return Auto mode."""
|
|
return self._device.state.auto_mode == "ON"
|
|
|
|
@property
|
|
def angle_low(self):
|
|
"""Return angle high."""
|
|
return int(self._device.state.oscillation_angle_low)
|
|
|
|
@property
|
|
def angle_high(self):
|
|
"""Return angle low."""
|
|
return int(self._device.state.oscillation_angle_high)
|
|
|
|
@property
|
|
def flow_direction_front(self):
|
|
"""Return frontal flow direction."""
|
|
return self._device.state.front_direction == "ON"
|
|
|
|
@property
|
|
def timer(self):
|
|
"""Return timer."""
|
|
return self._device.state.sleep_timer
|
|
|
|
@property
|
|
def hepa_filter(self):
|
|
"""Return the HEPA filter state."""
|
|
return int(self._device.state.hepa_filter_state)
|
|
|
|
@property
|
|
def carbon_filter(self):
|
|
"""Return the carbon filter state."""
|
|
if self._device.state.carbon_filter_state == "INV":
|
|
return self._device.state.carbon_filter_state
|
|
return int(self._device.state.carbon_filter_state)
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict:
|
|
"""Return optional state attributes."""
|
|
return {
|
|
**super().extra_state_attributes,
|
|
ATTR_ANGLE_LOW: self.angle_low,
|
|
ATTR_ANGLE_HIGH: self.angle_high,
|
|
ATTR_FLOW_DIRECTION_FRONT: self.flow_direction_front,
|
|
ATTR_TIMER: self.timer,
|
|
ATTR_HEPA_FILTER: self.hepa_filter,
|
|
ATTR_CARBON_FILTER: self.carbon_filter,
|
|
}
|