From e78709c5f501592c630186d866268ac01d48f9ea Mon Sep 17 00:00:00 2001 From: etheralm <8655564+etheralm@users.noreply.github.com> Date: Mon, 1 Apr 2019 19:57:11 +0200 Subject: [PATCH] Add support for Dyson Purecool 2018 Air Purifiers models TP04 and DP04 (#22215) * initial commit initial commit rewrite tests fix merge issue with fan component fix merge issue with fan component * correct line length * change to sync_setup_component for tests * rename services and move services.yaml * move hepa and carbon filter state from sensor to fan * add test for duplicate entities * fix method call tests * fix docstring --- homeassistant/components/dyson/__init__.py | 6 +- homeassistant/components/dyson/climate.py | 15 +- homeassistant/components/dyson/fan.py | 409 ++++++++++++++-- homeassistant/components/dyson/sensor.py | 9 +- homeassistant/components/dyson/services.yaml | 64 +++ homeassistant/components/dyson/vacuum.py | 16 +- homeassistant/components/fan/services.yaml | 10 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 4 +- tests/components/dyson/test_climate.py | 15 +- tests/components/dyson/test_fan.py | 484 +++++++++++++++++-- tests/components/dyson/test_init.py | 30 +- tests/components/dyson/test_sensor.py | 9 +- tests/components/dyson/test_vacuum.py | 4 +- 15 files changed, 946 insertions(+), 133 deletions(-) create mode 100644 homeassistant/components/dyson/services.yaml diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index c2e56436bd8..eccf8aac364 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -3,12 +3,12 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME) from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['libpurecoollink==0.4.2'] +REQUIREMENTS = ['libpurecool==0.5.0'] _LOGGER = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def setup(hass, config): if DYSON_DEVICES not in hass.data: hass.data[DYSON_DEVICES] = [] - from libpurecoollink.dyson import DysonAccount + from libpurecool.dyson import DysonAccount dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME), config[DOMAIN].get(CONF_PASSWORD), config[DOMAIN].get(CONF_LANGUAGE)) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 3e5c976b1f4..a24d011623b 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -11,7 +11,6 @@ from homeassistant.components.climate.const import ( STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS - from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) @@ -30,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink + from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink # Get Dyson Devices from parent component. add_devices( [DysonPureHotCoolLinkDevice(device) @@ -54,7 +53,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): def on_message(self, message): """Call when new messages received from the climate.""" - from libpurecoollink.dyson_pure_state import DysonPureHotCoolState + from libpurecool.dyson_pure_state import DysonPureHotCoolState if isinstance(message, DysonPureHotCoolState): _LOGGER.debug("Message received for climate device %s : %s", @@ -109,7 +108,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - from libpurecoollink.const import HeatMode, HeatState + from libpurecool.const import HeatMode, HeatState if self._device.state.heat_mode == HeatMode.HEAT_ON.value: if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value: return STATE_HEAT @@ -124,7 +123,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): @property def current_fan_mode(self): """Return the fan setting.""" - from libpurecoollink.const import FocusMode + from libpurecool.const import FocusMode if self._device.state.focus_mode == FocusMode.FOCUS_ON.value: return STATE_FOCUS return STATE_DIFFUSE @@ -144,7 +143,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): # Limit the target temperature into acceptable range. target_temp = min(self.max_temp, target_temp) target_temp = max(self.min_temp, target_temp) - from libpurecoollink.const import HeatTarget, HeatMode + from libpurecool.const import HeatTarget, HeatMode self._device.set_configuration( heat_target=HeatTarget.celsius(target_temp), heat_mode=HeatMode.HEAT_ON) @@ -152,7 +151,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): def set_fan_mode(self, fan_mode): """Set new fan mode.""" _LOGGER.debug("Set %s focus mode %s", self.name, fan_mode) - from libpurecoollink.const import FocusMode + from libpurecool.const import FocusMode if fan_mode == STATE_FOCUS: self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON) elif fan_mode == STATE_DIFFUSE: @@ -161,7 +160,7 @@ class DysonPureHotCoolLinkDevice(ClimateDevice): def set_operation_mode(self, operation_mode): """Set operation mode.""" _LOGGER.debug("Set %s heat mode %s", self.name, operation_mode) - from libpurecoollink.const import HeatMode + from libpurecool.const import HeatMode if operation_mode == STATE_HEAT: self._device.set_configuration(heat_mode=HeatMode.HEAT_ON) elif operation_mode == STATE_COOL: diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 743d301df42..0140378968b 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -7,65 +7,150 @@ import logging import voluptuous as vol -from homeassistant.components.fan import ( - DOMAIN, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity) -from homeassistant.const import CONF_ENTITY_ID import homeassistant.helpers.config_validation as cv - +from homeassistant.components.fan import ( + SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, + SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) +from homeassistant.const import ATTR_ENTITY_ID from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) -CONF_NIGHT_MODE = 'night_mode' - -ATTR_IS_NIGHT_MODE = 'is_night_mode' -ATTR_IS_AUTO_MODE = 'is_auto_mode' +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' DEPENDENCIES = ['dyson'] +DYSON_DOMAIN = 'dyson' DYSON_FAN_DEVICES = 'dyson_fan_devices' -SERVICE_SET_NIGHT_MODE = 'dyson_set_night_mode' +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' DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({ - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_NIGHT_MODE): cv.boolean, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_NIGHT_MODE): cv.boolean, +}) + +SET_AUTO_MODE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_AUTO_MODE): cv.boolean, +}) + +SET_ANGLE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ANGLE_LOW): cv.positive_int, + vol.Required(ATTR_ANGLE_HIGH): cv.positive_int +}) + +SET_FLOW_DIRECTION_FRONT_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean +}) + +SET_TIMER_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TIMER): cv.positive_int +}) + +SET_DYSON_SPEED_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DYSON_SPEED): cv.positive_int }) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson fan components.""" - from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink + from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + from libpurecool.dyson_pure_cool import DysonPureCool + + 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 - for device in [d for d in hass.data[DYSON_DEVICES] if - isinstance(d, DysonPureCoolLink)]: - dyson_entity = DysonPureCoolLinkDevice(hass, device) - hass.data[DYSON_FAN_DEVICES].append(dyson_entity) + 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 = DysonPureCoolDevice(device) + hass.data[DYSON_FAN_DEVICES].append(dyson_entity) + elif isinstance(device, DysonPureCoolLink): + dyson_entity = DysonPureCoolLinkDevice(hass, device) + hass.data[DYSON_FAN_DEVICES].append(dyson_entity) add_entities(hass.data[DYSON_FAN_DEVICES]) def service_handle(service): """Handle the Dyson services.""" - entity_id = service.data.get(CONF_ENTITY_ID) - night_mode = service.data.get(CONF_NIGHT_MODE) - fan_device = next([fan for fan in hass.data[DYSON_FAN_DEVICES] if - fan.entity_id == entity_id].__iter__(), None) + entity_id = service.data[ATTR_ENTITY_ID] + fan_device = next((fan for fan in hass.data[DYSON_FAN_DEVICES] if + fan.entity_id == entity_id), None) if fan_device is None: _LOGGER.warning("Unable to find Dyson fan device %s", str(entity_id)) return if service.service == SERVICE_SET_NIGHT_MODE: - fan_device.night_mode(night_mode) + fan_device.set_night_mode(service.data[ATTR_NIGHT_MODE]) + + if service.service == SERVICE_SET_AUTO_MODE: + fan_device.set_auto_mode(service.data[ATTR_AUTO_MODE]) + + if service.service == SERVICE_SET_ANGLE: + fan_device.set_angle(service.data[ATTR_ANGLE_LOW], + service.data[ATTR_ANGLE_HIGH]) + + if service.service == SERVICE_SET_FLOW_DIRECTION_FRONT: + fan_device.set_flow_direction_front( + service.data[ATTR_FLOW_DIRECTION_FRONT]) + + if service.service == SERVICE_SET_TIMER: + fan_device.set_timer(service.data[ATTR_TIMER]) + + if service.service == SERVICE_SET_DYSON_SPEED: + fan_device.set_dyson_speed(service.data[ATTR_DYSON_SPEED]) # Register dyson service(s) hass.services.register( - DOMAIN, SERVICE_SET_NIGHT_MODE, service_handle, + DYSON_DOMAIN, SERVICE_SET_NIGHT_MODE, service_handle, schema=DYSON_SET_NIGHT_MODE_SCHEMA) + if has_purecool_devices: + hass.services.register( + DYSON_DOMAIN, SERVICE_SET_AUTO_MODE, service_handle, + schema=SET_AUTO_MODE_SCHEMA) + + hass.services.register( + DYSON_DOMAIN, SERVICE_SET_ANGLE, service_handle, + schema=SET_ANGLE_SCHEMA) + + hass.services.register( + DYSON_DOMAIN, SERVICE_SET_FLOW_DIRECTION_FRONT, service_handle, + schema=SET_FLOW_DIRECTION_FRONT_SCHEMA) + + hass.services.register( + DYSON_DOMAIN, SERVICE_SET_TIMER, service_handle, + schema=SET_TIMER_SCHEMA) + + hass.services.register( + DYSON_DOMAIN, SERVICE_SET_DYSON_SPEED, service_handle, + schema=SET_DYSON_SPEED_SCHEMA) class DysonPureCoolLinkDevice(FanEntity): @@ -84,7 +169,7 @@ class DysonPureCoolLinkDevice(FanEntity): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecoollink.dyson_pure_state import DysonPureCoolState + from libpurecool.dyson_pure_state import DysonPureCoolState if isinstance(message, DysonPureCoolState): _LOGGER.debug("Message received for fan device %s: %s", self.name, @@ -103,7 +188,7 @@ class DysonPureCoolLinkDevice(FanEntity): def set_speed(self, speed: str) -> None: """Set the speed of the fan. Never called ??.""" - from libpurecoollink.const import FanSpeed, FanMode + from libpurecool.const import FanSpeed, FanMode _LOGGER.debug("Set fan speed to: %s", speed) @@ -116,7 +201,7 @@ class DysonPureCoolLinkDevice(FanEntity): def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" - from libpurecoollink.const import FanSpeed, FanMode + from libpurecool.const import FanSpeed, FanMode _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) if speed: @@ -132,14 +217,14 @@ class DysonPureCoolLinkDevice(FanEntity): def turn_off(self, **kwargs) -> None: """Turn off the fan.""" - from libpurecoollink.const import FanMode + from libpurecool.const import FanMode _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" - from libpurecoollink.const import Oscillation + from libpurecool.const import Oscillation _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) @@ -166,7 +251,7 @@ class DysonPureCoolLinkDevice(FanEntity): @property def speed(self) -> str: """Return the current speed.""" - from libpurecoollink.const import FanSpeed + from libpurecool.const import FanSpeed if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -180,13 +265,13 @@ class DysonPureCoolLinkDevice(FanEntity): return None @property - def is_night_mode(self): + def night_mode(self): """Return Night mode.""" return self._device.state.night_mode == "ON" - def night_mode(self, night_mode: bool) -> None: + def set_night_mode(self, night_mode: bool) -> None: """Turn fan in night mode.""" - from libpurecoollink.const import NightMode + from libpurecool.const import NightMode _LOGGER.debug("Set %s night mode %s", self.name, night_mode) if night_mode: @@ -195,13 +280,13 @@ class DysonPureCoolLinkDevice(FanEntity): self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF) @property - def is_auto_mode(self): + def auto_mode(self): """Return auto mode.""" return self._device.state.fan_mode == "AUTO" - def auto_mode(self, auto_mode: bool) -> None: + def set_auto_mode(self, auto_mode: bool) -> None: """Turn fan in auto mode.""" - from libpurecoollink.const import FanMode + from libpurecool.const import FanMode _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) if auto_mode: @@ -212,7 +297,7 @@ class DysonPureCoolLinkDevice(FanEntity): @property def speed_list(self) -> list: """Get the list of available speeds.""" - from libpurecoollink.const import FanSpeed + from libpurecool.const import FanSpeed supported_speeds = [ FanSpeed.FAN_SPEED_AUTO.value, @@ -239,6 +324,256 @@ class DysonPureCoolLinkDevice(FanEntity): def device_state_attributes(self) -> dict: """Return optional state attributes.""" return { - ATTR_IS_NIGHT_MODE: self.is_night_mode, - ATTR_IS_AUTO_MODE: self.is_auto_mode + ATTR_NIGHT_MODE: self.night_mode, + ATTR_AUTO_MODE: self.auto_mode } + + +class DysonPureCoolDevice(FanEntity): + """Representation of a Dyson Purecool (TP04/DP04) fan.""" + + def __init__(self, device): + """Initialize the fan.""" + self._device = device + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.hass.async_add_executor_job( + self._device.add_message_listener, self.on_message) + + def on_message(self, message): + """Call when new messages received from the fan.""" + from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State + + if isinstance(message, DysonPureCoolV2State): + _LOGGER.debug("Message received for fan device %s: %s", self.name, + message) + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the display name of this fan.""" + return self._device.name + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn on the fan.""" + _LOGGER.debug("Turn on fan %s", self.name) + + if speed is not None: + self.set_speed(speed) + else: + self._device.turn_on() + + def set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + from libpurecool.const import FanSpeed + if speed == SPEED_LOW: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) + elif speed == SPEED_MEDIUM: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_7) + elif speed == SPEED_HIGH: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) + + 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: str = None) -> None: + """Set the exact speed of the purecool fan.""" + from libpurecool.const import FanSpeed + + _LOGGER.debug("Set exact speed for fan %s", self.name) + + fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + self._device.set_fan_speed(fan_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.""" + if self._device.state: + return self._device.state.fan_power == "ON" + + @property + def speed(self): + """Return the current speed.""" + from libpurecool.const import FanSpeed + + speed_map = {FanSpeed.FAN_SPEED_1.value: SPEED_LOW, + FanSpeed.FAN_SPEED_2.value: SPEED_LOW, + FanSpeed.FAN_SPEED_3.value: SPEED_LOW, + FanSpeed.FAN_SPEED_4.value: SPEED_LOW, + FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_9.value: SPEED_HIGH} + + return speed_map[self._device.state.speed] + + @property + def dyson_speed(self): + """Return the current speed.""" + from libpurecool.const import FanSpeed + + if self._device.state: + if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: + return self._device.state.speed + return int(self._device.state.speed) + + @property + def night_mode(self): + """Return Night mode.""" + return self._device.state.night_mode == "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.""" + return int(self._device.state.carbon_filter_state) + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + @property + def dyson_speed_list(self) -> list: + """Get the list of available dyson speeds.""" + from libpurecool.const import FanSpeed + return [ + int(FanSpeed.FAN_SPEED_1.value), + int(FanSpeed.FAN_SPEED_2.value), + int(FanSpeed.FAN_SPEED_3.value), + int(FanSpeed.FAN_SPEED_4.value), + int(FanSpeed.FAN_SPEED_5.value), + int(FanSpeed.FAN_SPEED_6.value), + int(FanSpeed.FAN_SPEED_7.value), + int(FanSpeed.FAN_SPEED_8.value), + int(FanSpeed.FAN_SPEED_9.value), + int(FanSpeed.FAN_SPEED_10.value), + ] + + @property + def device_serial(self): + """Return fan's serial number.""" + return self._device.serial + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_OSCILLATE | \ + SUPPORT_SET_SPEED + + @property + def device_state_attributes(self) -> dict: + """Return optional state attributes.""" + return { + ATTR_NIGHT_MODE: self.night_mode, + ATTR_AUTO_MODE: self.auto_mode, + 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, + ATTR_DYSON_SPEED: self.dyson_speed, + ATTR_DYSON_SPEED_LIST: self.dyson_speed_list + } diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index ed8987f75c2..abf06f15437 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -37,9 +37,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] unit = hass.config.units.temperature_unit # Get Dyson Devices from parent component - from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink - for device in [d for d in hass.data[DYSON_DEVICES] if - isinstance(d, DysonPureCoolLink)]: + from libpurecool.dyson_pure_cool import DysonPureCool + from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + + for device in [d for d in hass.data[DYSON_DEVICES] + if isinstance(d, DysonPureCoolLink) and + not isinstance(d, DysonPureCool)]: devices.append(DysonFilterLifeSensor(device)) devices.append(DysonDustSensor(device)) devices.append(DysonHumiditySensor(device)) diff --git a/homeassistant/components/dyson/services.yaml b/homeassistant/components/dyson/services.yaml new file mode 100644 index 00000000000..a93b15b4304 --- /dev/null +++ b/homeassistant/components/dyson/services.yaml @@ -0,0 +1,64 @@ +# Describes the format for available fan services + +set_night_mode: + description: Set the fan in night mode. + fields: + entity_id: + description: Name(s) of the entities to enable/disable night mode + example: 'fan.living_room' + night_mode: + description: Night mode status + example: true + +set_auto_mode: + description: Set the fan in auto mode. + fields: + entity_id: + description: Name(s) of the entities to enable/disable auto mode + example: 'fan.living_room' + auto_mode: + description: Auto mode status + example: true + +set_angle: + description: Set the oscillation angle of the selected fan(s). + fields: + entity_id: + description: Name(s) of the entities for which to set the angle + example: 'fan.living_room' + angle_low: + description: The angle at which the oscillation should start + example: 1 + angle_high: + description: The angle at which the oscillation should end + example: 255 + +flow_direction_front: + description: Set the fan flow direction. + fields: + entity_id: + description: Name(s) of the entities to set frontal flow direction for + example: 'fan.living_room' + flow_direction_front: + description: Frontal flow direction + example: true + +set_timer: + description: Set the sleep timer. + fields: + entity_id: + description: Name(s) of the entities to set the sleep timer for + example: 'fan.living_room' + timer: + description: The value in minutes to set the timer to, 0 to disable it + example: 30 + +set_speed: + description: Set the exact speed of the fan. + fields: + entity_id: + description: Name(s) of the entities to set the speed for + example: 'fan.living_room' + timer: + description: Speed + example: 1 \ No newline at end of file diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index 7902cfa1585..72c7b95562f 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -31,7 +31,7 @@ SUPPORT_DYSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson 360 Eye robot vacuum platform.""" - from libpurecoollink.dyson_360_eye import Dyson360Eye + from libpurecool.dyson_360_eye import Dyson360Eye _LOGGER.debug("Creating new Dyson 360 Eye robot vacuum") if DYSON_360_EYE_DEVICES not in hass.data: @@ -81,7 +81,7 @@ class Dyson360EyeDevice(VacuumDevice): @property def status(self): """Return the status of the vacuum cleaner.""" - from libpurecoollink.const import Dyson360EyeMode + from libpurecool.const import Dyson360EyeMode dyson_labels = { Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", @@ -106,7 +106,7 @@ class Dyson360EyeDevice(VacuumDevice): @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - from libpurecoollink.const import PowerMode + from libpurecool.const import PowerMode speed_labels = { PowerMode.MAX: "Max", PowerMode.QUIET: "Quiet" @@ -128,7 +128,7 @@ class Dyson360EyeDevice(VacuumDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - from libpurecoollink.const import Dyson360EyeMode + from libpurecool.const import Dyson360EyeMode return self._device.state.state in [ Dyson360EyeMode.FULL_CLEAN_INITIATED, @@ -149,7 +149,7 @@ class Dyson360EyeDevice(VacuumDevice): @property def battery_icon(self): """Return the battery icon for the vacuum cleaner.""" - from libpurecoollink.const import Dyson360EyeMode + from libpurecool.const import Dyson360EyeMode charging = self._device.state.state in [ Dyson360EyeMode.INACTIVE_CHARGING] @@ -158,7 +158,7 @@ class Dyson360EyeDevice(VacuumDevice): def turn_on(self, **kwargs): """Turn the vacuum on.""" - from libpurecoollink.const import Dyson360EyeMode + from libpurecool.const import Dyson360EyeMode _LOGGER.debug("Turn on device %s", self.name) if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: @@ -178,7 +178,7 @@ class Dyson360EyeDevice(VacuumDevice): def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - from libpurecoollink.const import PowerMode + from libpurecool.const import PowerMode _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) power_modes = { @@ -189,7 +189,7 @@ class Dyson360EyeDevice(VacuumDevice): def start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" - from libpurecoollink.const import Dyson360EyeMode + from libpurecool.const import Dyson360EyeMode if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: _LOGGER.debug("Resume device %s", self.name) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 35a81c7c934..16d3742d9ab 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -54,16 +54,6 @@ set_direction: description: The direction to rotate. Either 'forward' or 'reverse' example: 'forward' -dyson_set_night_mode: - description: Set the fan in night mode. - fields: - entity_id: - description: Name(s) of the entities to enable/disable night mode - example: 'fan.living_room' - night_mode: - description: Night mode status - example: true - xiaomi_miio_set_buzzer_on: description: Turn the buzzer on. fields: diff --git a/requirements_all.txt b/requirements_all.txt index 3c3eabad553..ea76277be4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ konnected==0.1.5 lakeside==0.12 # homeassistant.components.dyson -libpurecoollink==0.4.2 +libpurecool==0.5.0 # homeassistant.components.foscam.camera libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc51c45f849..b18ee2b5261 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -145,7 +145,7 @@ influxdb==5.2.0 jsonpath==0.75 # homeassistant.components.dyson -libpurecoollink==0.4.2 +libpurecool==0.5.0 # homeassistant.components.soundtouch.media_player libsoundtouch==0.7.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5cc347249f7..3180f7b8228 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """Generate an updated requirements_all.txt.""" +import fnmatch import importlib import os import pkgutil import re import sys -import fnmatch COMMENT_REQUIREMENTS = ( 'Adafruit-DHT', @@ -74,7 +74,7 @@ TEST_REQUIREMENTS = ( 'homematicip', 'influxdb', 'jsonpath', - 'libpurecoollink', + 'libpurecool', 'libsoundtouch', 'luftdaten', 'mbddns', diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 43ce6344ec4..778b3bdad49 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -2,12 +2,13 @@ import unittest from unittest import mock -from libpurecoollink.const import (FocusMode, HeatMode, HeatState, HeatTarget, - TiltState) -from libpurecoollink.dyson_pure_state import DysonPureHotCoolState -from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink -from homeassistant.components.dyson import climate as dyson +from libpurecool.const import (FocusMode, HeatMode, + HeatState, HeatTarget, TiltState) +from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink +from libpurecool.dyson_pure_state import DysonPureHotCoolState + from homeassistant.components import dyson as dyson_parent +from homeassistant.components.dyson import climate as dyson from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -110,9 +111,9 @@ class DysonTest(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_device_heat_on(), _get_device_cool()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_setup_component_with_parent_discovery(self, mocked_login, mocked_devices): """Test setup_component using discovery.""" diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index a04116f10f2..0a9469ae807 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -1,16 +1,28 @@ """Test the Dyson fan component.""" +import json import unittest from unittest import mock -from homeassistant.setup import setup_component +import asynctest +from libpurecool.const import FanSpeed, FanMode, 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 homeassistant.components.dyson.fan as dyson from homeassistant.components import dyson as dyson_parent -from homeassistant.components.dyson import DYSON_DEVICES, fan as dyson -from homeassistant.components.fan import (ATTR_SPEED, ATTR_SPEED_LIST, - ATTR_OSCILLATING) +from homeassistant.components.dyson import DYSON_DEVICES +from homeassistant.components.fan import (DOMAIN, ATTR_SPEED, ATTR_SPEED_LIST, + ATTR_OSCILLATING, SPEED_LOW, + SPEED_MEDIUM, SPEED_HIGH, + SERVICE_OSCILLATE) +from homeassistant.const import (SERVICE_TURN_ON, + SERVICE_TURN_OFF, + ATTR_ENTITY_ID) +from homeassistant.helpers import discovery +from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant -from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation -from libpurecoollink.dyson_pure_state import DysonPureCoolState -from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink class MockDysonState(DysonPureCoolState): @@ -21,6 +33,58 @@ class MockDysonState(DysonPureCoolState): pass +def _get_dyson_purecool_device(): + """Return a valid device as provided by the Dyson web services.""" + device = mock.Mock(spec=DysonPureCool) + device.serial = "XX-XXXXX-XX" + device.name = "Living room" + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.state = mock.Mock() + device.state.oscillation = "OION" + device.state.fan_power = "ON" + device.state.speed = FanSpeed.FAN_SPEED_AUTO.value + device.state.night_mode = "OFF" + device.state.auto_mode = "ON" + device.state.oscillation_angle_low = "0090" + device.state.oscillation_angle_high = "0180" + device.state.front_direction = "ON" + device.state.sleep_timer = 60 + device.state.hepa_filter_state = "0090" + device.state.carbon_filter_state = "0080" + return device + + +def _get_supported_speeds(): + return [ + int(FanSpeed.FAN_SPEED_1.value), + int(FanSpeed.FAN_SPEED_2.value), + int(FanSpeed.FAN_SPEED_3.value), + int(FanSpeed.FAN_SPEED_4.value), + int(FanSpeed.FAN_SPEED_5.value), + int(FanSpeed.FAN_SPEED_6.value), + int(FanSpeed.FAN_SPEED_7.value), + int(FanSpeed.FAN_SPEED_8.value), + int(FanSpeed.FAN_SPEED_9.value), + int(FanSpeed.FAN_SPEED_10.value), + ] + + +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "GB", + dyson_parent.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + } + ] + }} + + def _get_device_with_no_state(): """Return a device with no state.""" device = mock.Mock() @@ -64,8 +128,8 @@ def _get_device_on(): return device -class DysonTest(unittest.TestCase): - """Dyson Sensor component test class.""" +class DysonSetupTest(unittest.TestCase): + """Dyson component setup tests.""" def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" @@ -79,24 +143,39 @@ class DysonTest(unittest.TestCase): """Test setup component with no devices.""" self.hass.data[dyson.DYSON_DEVICES] = [] add_entities = mock.MagicMock() - dyson.setup_platform(self.hass, None, add_entities) + dyson.setup_platform(self.hass, None, add_entities, mock.Mock()) add_entities.assert_called_with([]) def test_setup_component(self): """Test setup component with devices.""" def _add_device(devices): - assert len(devices) == 1 + assert len(devices) == 2 assert devices[0].name == "Device_name" device_fan = _get_device_on() + device_purecool_fan = _get_dyson_purecool_device() device_non_fan = _get_device_off() - self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] + self.hass.data[dyson.DYSON_DEVICES] = [device_fan, + device_purecool_fan, + device_non_fan] dyson.setup_platform(self.hass, None, _add_device) - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + +class DysonTest(unittest.TestCase): + """Dyson fan component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_device_on()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_get_state_attributes(self, mocked_login, mocked_devices): """Test async added to hass.""" setup_component(self.hass, dyson_parent.DOMAIN, { @@ -108,18 +187,18 @@ class DysonTest(unittest.TestCase): }) self.hass.block_till_done() state = self.hass.states.get("{}.{}".format( - dyson.DOMAIN, + DOMAIN, mocked_devices.return_value[0].name)) - assert dyson.ATTR_IS_NIGHT_MODE in state.attributes - assert dyson.ATTR_IS_AUTO_MODE in state.attributes + assert dyson.ATTR_NIGHT_MODE in state.attributes + assert dyson.ATTR_AUTO_MODE in state.attributes assert ATTR_SPEED in state.attributes assert ATTR_SPEED_LIST in state.attributes assert ATTR_OSCILLATING in state.attributes - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_device_on()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_async_added_to_hass(self, mocked_login, mocked_devices): """Test async added to hass.""" setup_component(self.hass, dyson_parent.DOMAIN, { @@ -161,11 +240,11 @@ class DysonTest(unittest.TestCase): device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) assert not component.should_poll - component.night_mode(True) + component.set_night_mode(True) set_config = device.set_configuration set_config.assert_called_with(night_mode=NightMode.NIGHT_MODE_ON) - component.night_mode(False) + component.set_night_mode(False) set_config = device.set_configuration set_config.assert_called_with(night_mode=NightMode.NIGHT_MODE_OFF) @@ -173,22 +252,22 @@ class DysonTest(unittest.TestCase): """Test night mode.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.is_night_mode + assert not component.night_mode device = _get_device_off() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.is_night_mode + assert component.night_mode def test_dyson_turn_auto_mode(self): """Test turn on/off fan with auto mode.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) assert not component.should_poll - component.auto_mode(True) + component.set_auto_mode(True) set_config = device.set_configuration set_config.assert_called_with(fan_mode=FanMode.AUTO) - component.auto_mode(False) + component.set_auto_mode(False) set_config = device.set_configuration set_config.assert_called_with(fan_mode=FanMode.FAN) @@ -196,11 +275,11 @@ class DysonTest(unittest.TestCase): """Test auto mode.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.is_auto_mode + assert not component.auto_mode device = _get_device_auto() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.is_auto_mode + assert component.auto_mode def test_dyson_turn_on_speed(self): """Test turn on fan with specified speed.""" @@ -320,14 +399,355 @@ class DysonTest(unittest.TestCase): self.hass.data[DYSON_DEVICES] = [] dyson_device.entity_id = 'fan.living_room' self.hass.data[dyson.DYSON_FAN_DEVICES] = [dyson_device] - dyson.setup_platform(self.hass, None, mock.MagicMock()) + dyson.setup_platform(self.hass, None, + mock.MagicMock(), mock.MagicMock()) - self.hass.services.call(dyson.DOMAIN, dyson.SERVICE_SET_NIGHT_MODE, + self.hass.services.call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_NIGHT_MODE, {"entity_id": "fan.bed_room", "night_mode": True}, True) - assert not dyson_device.night_mode.called + assert dyson_device.set_night_mode.call_count == 0 - self.hass.services.call(dyson.DOMAIN, dyson.SERVICE_SET_NIGHT_MODE, + self.hass.services.call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_NIGHT_MODE, {"entity_id": "fan.living_room", "night_mode": True}, True) - dyson_device.night_mode.assert_called_with(True) + dyson_device.set_night_mode.assert_called_with(True) + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_turn_on(devices, login, hass): + """Test turn on.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.bed_room"}, True) + assert device.turn_on.call_count == 0 + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.living_room"}, True) + assert device.turn_on.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_speed(devices, login, hass): + """Test set speed.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.bed_room", + ATTR_SPEED: SPEED_LOW}, True) + assert device.set_fan_speed.call_count == 0 + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.living_room", + ATTR_SPEED: SPEED_LOW}, True) + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_4) + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.living_room", + ATTR_SPEED: SPEED_MEDIUM}, True) + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_7) + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.living_room", + ATTR_SPEED: SPEED_HIGH}, True) + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_10) + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_turn_off(devices, login, hass): + """Test turn off.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.bed_room"}, True) + assert device.turn_off.call_count == 0 + + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.living_room"}, True) + assert device.turn_off.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_dyson_speed(devices, login, hass): + """Test set exact dyson speed.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_DYSON_SPEED, + {ATTR_ENTITY_ID: "fan.bed_room", + dyson.ATTR_DYSON_SPEED: + int(FanSpeed.FAN_SPEED_2.value)}, + True) + assert device.set_fan_speed.call_count == 0 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_DYSON_SPEED, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_DYSON_SPEED: + int(FanSpeed.FAN_SPEED_2.value)}, + True) + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_2) + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_oscillate(devices, login, hass): + """Test set oscillation.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, + {ATTR_ENTITY_ID: "fan.bed_room", + ATTR_OSCILLATING: True}, True) + assert device.enable_oscillation.call_count == 0 + + await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, + {ATTR_ENTITY_ID: "fan.living_room", + ATTR_OSCILLATING: True}, True) + assert device.enable_oscillation.call_count == 1 + + await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, + {ATTR_ENTITY_ID: "fan.living_room", + ATTR_OSCILLATING: False}, True) + assert device.disable_oscillation.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_night_mode(devices, login, hass): + """Test set night mode.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + + await hass.async_block_till_done() + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_NIGHT_MODE, + {"entity_id": "fan.bed_room", + "night_mode": True}, True) + assert device.enable_night_mode.call_count == 0 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_NIGHT_MODE, + {"entity_id": "fan.living_room", + "night_mode": True}, True) + assert device.enable_night_mode.call_count == 1 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_NIGHT_MODE, + {"entity_id": "fan.living_room", + "night_mode": False}, True) + assert device.disable_night_mode.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_auto_mode(devices, login, hass): + """Test set auto mode.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_AUTO_MODE, + {ATTR_ENTITY_ID: "fan.bed_room", + dyson.ATTR_AUTO_MODE: True}, True) + assert device.enable_auto_mode.call_count == 0 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_AUTO_MODE, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_AUTO_MODE: True}, True) + assert device.enable_auto_mode.call_count == 1 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_AUTO_MODE, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_AUTO_MODE: False}, True) + assert device.disable_auto_mode.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_angle(devices, login, hass): + """Test set angle.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(dyson.DYSON_DOMAIN, dyson.SERVICE_SET_ANGLE, + {ATTR_ENTITY_ID: "fan.bed_room", + dyson.ATTR_ANGLE_LOW: 90, + dyson.ATTR_ANGLE_HIGH: 180}, True) + assert device.enable_oscillation.call_count == 0 + + await hass.services.async_call(dyson.DYSON_DOMAIN, dyson.SERVICE_SET_ANGLE, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_ANGLE_LOW: 90, + dyson.ATTR_ANGLE_HIGH: 180}, True) + device.enable_oscillation.assert_called_with(90, 180) + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_flow_direction_front(devices, login, hass): + """Test set frontal flow direction.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_FLOW_DIRECTION_FRONT, + {ATTR_ENTITY_ID: "fan.bed_room", + dyson.ATTR_FLOW_DIRECTION_FRONT: True}, + True) + assert device.enable_frontal_direction.call_count == 0 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_FLOW_DIRECTION_FRONT, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_FLOW_DIRECTION_FRONT: True}, + True) + assert device.enable_frontal_direction.call_count == 1 + + await hass.services.async_call(dyson.DYSON_DOMAIN, + dyson.SERVICE_SET_FLOW_DIRECTION_FRONT, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_FLOW_DIRECTION_FRONT: False}, + True) + assert device.disable_frontal_direction.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_set_timer(devices, login, hass): + """Test set timer.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call(dyson.DYSON_DOMAIN, dyson.SERVICE_SET_TIMER, + {ATTR_ENTITY_ID: "fan.bed_room", + dyson.ATTR_TIMER: 60}, + True) + assert device.enable_frontal_direction.call_count == 0 + + await hass.services.async_call(dyson.DYSON_DOMAIN, dyson.SERVICE_SET_TIMER, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_TIMER: 60}, + True) + device.enable_sleep_timer.assert_called_with(60) + + await hass.services.async_call(dyson.DYSON_DOMAIN, dyson.SERVICE_SET_TIMER, + {ATTR_ENTITY_ID: "fan.living_room", + dyson.ATTR_TIMER: 0}, + True) + assert device.disable_sleep_timer.call_count == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_attributes(devices, login, hass): + """Test state attributes.""" + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + fan_state = hass.states.get("fan.living_room") + attributes = fan_state.attributes + + assert fan_state.state == "on" + assert attributes[dyson.ATTR_NIGHT_MODE] is False + assert attributes[dyson.ATTR_AUTO_MODE] is True + assert attributes[dyson.ATTR_ANGLE_LOW] == 90 + assert attributes[dyson.ATTR_ANGLE_HIGH] == 180 + assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is True + assert attributes[dyson.ATTR_TIMER] == 60 + assert attributes[dyson.ATTR_HEPA_FILTER] == 90 + assert attributes[dyson.ATTR_CARBON_FILTER] == 80 + assert attributes[dyson.ATTR_DYSON_SPEED] == FanSpeed.FAN_SPEED_AUTO.value + assert attributes[ATTR_SPEED] == SPEED_MEDIUM + assert attributes[ATTR_OSCILLATING] is True + assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_update_state(devices, login, hass): + """Test state update.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + event = {"msg": "CURRENT-STATE", + "product-state": {"fpwr": "OFF", "fdir": "OFF", "auto": "OFF", + "oscs": "ON", "oson": "ON", "nmod": "OFF", + "rhtm": "ON", "fnst": "FAN", "ercd": "11E1", + "wacd": "NONE", "nmdv": "0004", "fnsp": "0002", + "bril": "0002", "corf": "ON", "cflr": "0085", + "hflr": "0095", "sltm": "OFF", "osal": "0045", + "osau": "0095", "ancp": "CUST"}} + device.state = DysonPureCoolV2State(json.dumps(event)) + + callback = device.add_message_listener.call_args_list[0][0][0] + callback(device.state) + await hass.async_block_till_done() + fan_state = hass.states.get("fan.living_room") + attributes = fan_state.attributes + + assert fan_state.state == "off" + assert attributes[dyson.ATTR_NIGHT_MODE] is False + assert attributes[dyson.ATTR_AUTO_MODE] is False + assert attributes[dyson.ATTR_ANGLE_LOW] == 45 + assert attributes[dyson.ATTR_ANGLE_HIGH] == 95 + assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is False + assert attributes[dyson.ATTR_TIMER] == "OFF" + assert attributes[dyson.ATTR_HEPA_FILTER] == 95 + assert attributes[dyson.ATTR_CARBON_FILTER] == 85 + assert attributes[dyson.ATTR_DYSON_SPEED] == \ + int(FanSpeed.FAN_SPEED_2.value) + assert attributes[ATTR_SPEED] is SPEED_LOW + assert attributes[ATTR_OSCILLATING] is False + assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_component_setup_only_once(devices, login, hass): + """Test if entities are created only once.""" + config = _get_config() + await async_setup_component(hass, dyson_parent.DOMAIN, config) + await hass.async_block_till_done() + discovery.load_platform(hass, "fan", dyson_parent.DOMAIN, {}, config) + await hass.async_block_till_done() + + fans = [fan for fan in hass.data[DOMAIN].entities + if fan.platform.platform_name == dyson_parent.DOMAIN] + + assert len(fans) == 1 + assert fans[0].device_serial == "XX-XXXXX-XX" diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py index 2e7b05b06cd..cc8c04a1559 100644 --- a/tests/components/dyson/test_init.py +++ b/tests/components/dyson/test_init.py @@ -43,7 +43,7 @@ class DysonTest(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=False) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=False) def test_dyson_login_failed(self, mocked_login): """Test if Dyson connection failed.""" dyson.setup(self.hass, {dyson.DOMAIN: { @@ -53,8 +53,8 @@ class DysonTest(unittest.TestCase): }}) assert mocked_login.call_count == 1 - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[]) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_login(self, mocked_login, mocked_devices): """Test valid connection to dyson web service.""" dyson.setup(self.hass, {dyson.DOMAIN: { @@ -67,9 +67,9 @@ class DysonTest(unittest.TestCase): assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 @mock.patch('homeassistant.helpers.discovery.load_platform') - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_custom_conf(self, mocked_login, mocked_devices, mocked_discovery): """Test device connection using custom configuration.""" @@ -89,9 +89,9 @@ class DysonTest(unittest.TestCase): assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 assert mocked_discovery.call_count == 4 - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_custom_conf_device_not_available(self, mocked_login, mocked_devices): """Test device connection with an invalid device.""" @@ -110,9 +110,9 @@ class DysonTest(unittest.TestCase): assert mocked_devices.call_count == 1 assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_error()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_custom_conf_device_error(self, mocked_login, mocked_devices): """Test device connection with device raising an exception.""" @@ -132,9 +132,9 @@ class DysonTest(unittest.TestCase): assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 @mock.patch('homeassistant.helpers.discovery.load_platform') - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_custom_conf_with_unknown_device(self, mocked_login, mocked_devices, mocked_discovery): @@ -156,9 +156,9 @@ class DysonTest(unittest.TestCase): assert mocked_discovery.call_count == 0 @mock.patch('homeassistant.helpers.discovery.load_platform') - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_discovery(self, mocked_login, mocked_devices, mocked_discovery): """Test device connection using discovery.""" @@ -174,9 +174,9 @@ class DysonTest(unittest.TestCase): assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 assert mocked_discovery.call_count == 4 - @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) - @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) def test_dyson_discovery_device_not_available(self, mocked_login, mocked_devices): """Test device connection with discovery and invalid device.""" diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 3218038c7e3..67c34d4d180 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -2,16 +2,17 @@ import unittest from unittest import mock +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + +from homeassistant.components.dyson import sensor as dyson from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, \ STATE_OFF -from homeassistant.components.dyson import sensor as dyson from tests.common import get_test_home_assistant -from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink def _get_device_without_state(): """Return a valid device provide by Dyson web services.""" - device = mock.Mock(spec=DysonPureCoolLink) + device = mock.Mock() device.name = "Device_name" device.state = None device.environmental_state = None @@ -20,7 +21,7 @@ def _get_device_without_state(): def _get_with_state(): """Return a valid device with state values.""" - device = mock.Mock() + device = mock.Mock(spec=DysonPureCoolLink) device.name = "Device_name" device.state = mock.Mock() device.state.filter_life = 100 diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py index 05ad8cf0db7..cdf76c975ae 100644 --- a/tests/components/dyson/test_vacuum.py +++ b/tests/components/dyson/test_vacuum.py @@ -2,8 +2,8 @@ import unittest from unittest import mock -from libpurecoollink.dyson_360_eye import Dyson360Eye -from libpurecoollink.const import Dyson360EyeMode, PowerMode +from libpurecool.const import Dyson360EyeMode, PowerMode +from libpurecool.dyson_360_eye import Dyson360Eye from homeassistant.components.dyson import vacuum as dyson from homeassistant.components.dyson.vacuum import Dyson360EyeDevice