From 5931f6598a98af915c7c4435709a76de4b7093f5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 6 May 2022 09:05:15 +0200 Subject: [PATCH] Add tests for Sensibo (#71148) * Initial commit * Check temperature missing * fix temp is none * Fix parallell * Commit to save * Fix tests * Fix test_init * assert 25 * Adjustments tests * Small removal * Cleanup * no hass.data * Adjustment test_coordinator * Minor change test_coordinator --- .coveragerc | 6 - tests/components/sensibo/__init__.py | 5 + tests/components/sensibo/conftest.py | 45 ++ tests/components/sensibo/response.py | 400 ++++++++++++ .../components/sensibo/test_binary_sensor.py | 52 ++ tests/components/sensibo/test_climate.py | 601 ++++++++++++++++++ tests/components/sensibo/test_coordinator.py | 91 +++ tests/components/sensibo/test_diagnostics.py | 48 ++ tests/components/sensibo/test_entity.py | 132 ++++ tests/components/sensibo/test_init.py | 132 ++++ 10 files changed, 1506 insertions(+), 6 deletions(-) create mode 100644 tests/components/sensibo/conftest.py create mode 100644 tests/components/sensibo/response.py create mode 100644 tests/components/sensibo/test_binary_sensor.py create mode 100644 tests/components/sensibo/test_climate.py create mode 100644 tests/components/sensibo/test_coordinator.py create mode 100644 tests/components/sensibo/test_diagnostics.py create mode 100644 tests/components/sensibo/test_entity.py create mode 100644 tests/components/sensibo/test_init.py diff --git a/.coveragerc b/.coveragerc index 3464b0df1be..353d2f07d06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1017,12 +1017,6 @@ omit = homeassistant/components/senseme/fan.py homeassistant/components/senseme/light.py homeassistant/components/senseme/switch.py - homeassistant/components/sensibo/__init__.py - homeassistant/components/sensibo/binary_sensor.py - homeassistant/components/sensibo/climate.py - homeassistant/components/sensibo/coordinator.py - homeassistant/components/sensibo/diagnostics.py - homeassistant/components/sensibo/entity.py homeassistant/components/sensibo/number.py homeassistant/components/sensibo/select.py homeassistant/components/sensibo/sensor.py diff --git a/tests/components/sensibo/__init__.py b/tests/components/sensibo/__init__.py index 8dd2ed661bc..da585f8d1e8 100644 --- a/tests/components/sensibo/__init__.py +++ b/tests/components/sensibo/__init__.py @@ -1 +1,6 @@ """Tests for the Sensibo integration.""" +from __future__ import annotations + +from homeassistant.const import CONF_API_KEY + +ENTRY_CONFIG = {CONF_API_KEY: "1234567890"} diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py new file mode 100644 index 00000000000..9380ee6ab6b --- /dev/null +++ b/tests/components/sensibo/conftest.py @@ -0,0 +1,45 @@ +"""Fixtures for the Sensibo integration.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG +from .response import DATA_FROM_API + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def load_int(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Sensibo integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="username", + version=2, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/sensibo/response.py b/tests/components/sensibo/response.py new file mode 100644 index 00000000000..e6b39b81881 --- /dev/null +++ b/tests/components/sensibo/response.py @@ -0,0 +1,400 @@ +"""Test api response for the Sensibo integration.""" +from __future__ import annotations + +from pysensibo.model import MotionSensor, SensiboData, SensiboDevice + +DATA_FROM_API = SensiboData( + raw={ + "status": "success", + "result": [ + { + "id": "ABC999111", + "qrId": "AAAAAAAAAA", + "room": {"uid": "99TT99TT", "name": "Hallway", "icon": "Lounge"}, + "acState": { + "timestamp": { + "time": "2022-04-30T19:58:15.544787Z", + "secondsAgo": 0, + }, + "on": False, + "mode": "fan", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "location": { + "id": "ZZZZZZZZZZZZ", + "name": "Home", + "latLon": [58.9806976, 20.5864297], + "address": ["Sealand 99", "Some county"], + "country": "United Country", + "createTime": { + "time": "2020-03-21T15:44:15Z", + "secondsAgo": 66543240, + }, + "updateTime": None, + "features": [], + "geofenceTriggerRadius": 200, + "subscription": None, + "technician": None, + "shareAnalytics": False, + "occupancy": "n/a", + }, + "accessPoint": {"ssid": "SENSIBO-I-99999", "password": None}, + "macAddress": "00:02:00:B6:00:00", + "autoOffMinutes": None, + "autoOffEnabled": False, + "antiMoldTimer": None, + "antiMoldConfig": None, + } + ], + }, + parsed={ + "ABC999111": SensiboDevice( + id="ABC999111", + mac="00:02:00:B6:00:00", + name="Hallway", + ac_states={ + "timestamp": {"time": "2022-04-30T19:58:15.544787Z", "secondsAgo": 0}, + "on": False, + "mode": "heat", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + temp=22.4, + humidity=38, + target_temp=25, + hvac_mode="heat", + device_on=True, + fan_mode="high", + swing_mode="stopped", + horizontal_swing_mode="stopped", + light_mode="on", + available=True, + hvac_modes=["cool", "heat", "dry", "auto", "fan", "off"], + fan_modes=["quiet", "low", "medium"], + swing_modes=[ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + horizontal_swing_modes=[ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + light_modes=["on", "off"], + temp_unit="C", + temp_list=[18, 19, 20], + temp_step=1, + active_features=[ + "timestamp", + "on", + "mode", + "fanLevel", + "swing", + "targetTemperature", + "horizontalSwing", + "light", + ], + full_features={ + "targetTemperature", + "fanLevel", + "swing", + "horizontalSwing", + "light", + }, + state="heat", + fw_ver="SKY30046", + fw_ver_available="SKY30046", + fw_type="esp8266ex", + model="skyv2", + calibration_temp=0.1, + calibration_hum=0.1, + full_capabilities={ + "modes": { + "cool": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 64, + 66, + 68, + ], + }, + "C": { + "isNative": True, + "values": [ + 18, + 19, + 20, + ], + }, + }, + "fanLevels": [ + "quiet", + "low", + "medium", + ], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "heat": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 63, + 64, + 66, + ], + }, + "C": { + "isNative": True, + "values": [ + 17, + 18, + 19, + ], + }, + }, + "fanLevels": ["quiet", "low", "medium"], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "dry": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 64, + 66, + 68, + ], + }, + "C": { + "isNative": True, + "values": [ + 18, + 19, + 20, + ], + }, + }, + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "auto": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 64, + 66, + 68, + ], + }, + "C": { + "isNative": True, + "values": [ + 18, + 19, + 20, + ], + }, + }, + "fanLevels": [ + "quiet", + "low", + "medium", + ], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "fan": { + "temperatures": {}, + "fanLevels": [ + "quiet", + "low", + ], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + } + }, + motion_sensors={ + "AABBCC": MotionSensor( + id="AABBCC", + alive=True, + motion=True, + fw_ver="V17", + fw_type="nrf52", + is_main_sensor=True, + battery_voltage=3000, + humidity=57, + temperature=23.9, + model="motion_sensor", + rssi=-72, + ) + }, + pm25=None, + room_occupied=True, + update_available=False, + schedules={}, + pure_boost_enabled=None, + pure_sensitivity=None, + pure_ac_integration=None, + pure_geo_integration=None, + pure_measure_integration=None, + timer_on=False, + timer_id=None, + timer_state_on=None, + timer_time=None, + smart_on=False, + smart_type="temperature", + smart_low_temp_threshold=0.0, + smart_high_temp_threshold=27.5, + smart_low_state={ + "on": True, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + smart_high_state={ + "on": True, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + filter_clean=False, + filter_last_reset="2022-03-12T15:24:26Z", + ), + "AAZZAAZZ": SensiboDevice( + id="AAZZAAZZ", + mac="00:01:00:01:00:01", + name="Kitchen", + ac_states={ + "timestamp": {"time": "2022-04-30T19:58:15.568753Z", "secondsAgo": 0}, + "on": False, + "mode": "fan", + "fanLevel": "low", + "light": "on", + }, + temp=None, + humidity=None, + target_temp=None, + hvac_mode="off", + device_on=False, + fan_mode="low", + swing_mode=None, + horizontal_swing_mode=None, + light_mode="on", + available=True, + hvac_modes=["fan", "off"], + fan_modes=["low", "high"], + swing_modes=None, + horizontal_swing_modes=None, + light_modes=["on", "dim", "off"], + temp_unit="C", + temp_list=[0, 1], + temp_step=1, + active_features=["timestamp", "on", "mode", "fanLevel", "light"], + full_features={"light", "targetTemperature", "fanLevel"}, + state="off", + fw_ver="PUR00111", + fw_ver_available="PUR00111", + fw_type="pure-esp32", + model="pure", + calibration_temp=0.0, + calibration_hum=0.0, + full_capabilities={ + "modes": { + "fan": { + "temperatures": {}, + "fanLevels": ["low", "high"], + "light": ["on", "dim", "off"], + } + } + }, + motion_sensors={}, + pm25=1, + room_occupied=None, + update_available=False, + schedules={}, + pure_boost_enabled=False, + pure_sensitivity="N", + pure_ac_integration=False, + pure_geo_integration=False, + pure_measure_integration=True, + timer_on=None, + timer_id=None, + timer_state_on=None, + timer_time=None, + smart_on=None, + smart_type=None, + smart_low_temp_threshold=None, + smart_high_temp_threshold=None, + smart_low_state=None, + smart_high_state=None, + filter_clean=False, + filter_last_reset="2022-04-23T15:58:45Z", + ), + }, +) diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py new file mode 100644 index 00000000000..cbf38ff27b0 --- /dev/null +++ b/tests/components/sensibo/test_binary_sensor.py @@ -0,0 +1,52 @@ +"""The test for the sensibo binary sensor platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pytest import MonkeyPatch + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from .response import DATA_FROM_API + +from tests.common import async_fire_time_changed + + +async def test_binary_sensor( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: MonkeyPatch +) -> None: + """Test the Sensibo binary sensor.""" + + state1 = hass.states.get("binary_sensor.hallway_motion_sensor_alive") + state2 = hass.states.get("binary_sensor.hallway_motion_sensor_main_sensor") + state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") + state4 = hass.states.get("binary_sensor.hallway_room_occupied") + assert state1.state == "on" + assert state2.state == "on" + assert state3.state == "on" + assert state4.state == "on" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False + ) + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"].motion_sensors["AABBCC"], "motion", False + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("binary_sensor.hallway_motion_sensor_alive") + state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") + assert state1.state == "off" + assert state3.state == "off" diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py new file mode 100644 index 00000000000..8b087da5d95 --- /dev/null +++ b/tests/components/sensibo/test_climate.py @@ -0,0 +1,601 @@ +"""The test for the sensibo binary sensor platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +import pytest +from voluptuous import MultipleInvalid + +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.sensibo.climate import SERVICE_ASSUME_STATE +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_STATE, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from .response import DATA_FROM_API + +from tests.common import async_fire_time_changed + + +async def test_climate(hass: HomeAssistant, load_int: ConfigEntry) -> None: + """Test the Sensibo climate.""" + + state1 = hass.states.get("climate.hallway") + state2 = hass.states.get("climate.kitchen") + + assert state1.state == "heat" + assert state1.attributes == { + "hvac_modes": [ + "cool", + "heat", + "dry", + "heat_cool", + "fan_only", + "off", + ], + "min_temp": 18, + "max_temp": 20, + "target_temp_step": 1, + "fan_modes": ["quiet", "low", "medium"], + "swing_modes": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "current_temperature": 22.4, + "temperature": 25, + "current_humidity": 38, + "fan_mode": "high", + "swing_mode": "stopped", + "friendly_name": "Hallway", + "supported_features": 41, + } + + assert state2.state == "off" + + +async def test_climate_fan( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate fan service.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["fan_mode"] == "high" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["fan_mode"] == "low" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "swing", + "targetTemperature", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + await hass.async_block_till_done() + + state3 = hass.states.get("climate.hallway") + assert state3.attributes["fan_mode"] == "low" + + +async def test_climate_swing( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate swing service.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["swing_mode"] == "stopped" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["swing_mode"] == "fixedTop" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "targetTemperature", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + blocking=True, + ) + await hass.async_block_till_done() + + state3 = hass.states.get("climate.hallway") + assert state3.attributes["swing_mode"] == "fixedTop" + + +async def test_climate_temperatures( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate temperature service.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["temperature"] == 25 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 18 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 18.5}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 18 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 24}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "swing", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + +async def test_climate_temperature_is_none( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate temperature service no temperature provided.""" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "fanLevel", + "targetTemperature", + "swing", + "horizontalSwing", + "light", + ], + ) + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "target_temp", + 25, + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["temperature"] == 25 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ): + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 20, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 25 + + +async def test_climate_hvac_mode( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate hvac mode service.""" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "fanLevel", + "targetTemperature", + "swing", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "off"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "off" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", False) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "heat"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "heat" + + +async def test_climate_on_off( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate on/off service.""" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "off" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "heat" + + +async def test_climate_service_failed( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate service failed.""" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Error", "failureReason": "Did not work"}}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "heat" + + +async def test_climate_assumed_state( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate assumed state service.""" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ASSUME_STATE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_STATE: "off"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "off" diff --git a/tests/components/sensibo/test_coordinator.py b/tests/components/sensibo/test_coordinator.py new file mode 100644 index 00000000000..703eb8b184b --- /dev/null +++ b/tests/components/sensibo/test_coordinator.py @@ -0,0 +1,91 @@ +"""The test for the sensibo coordinator.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo.model import SensiboData +import pytest + +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from . import ENTRY_CONFIG +from .response import DATA_FROM_API + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo coordinator with errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="username", + version=2, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + ) as mock_data, patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + mock_data.return_value = DATA_FROM_API + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == "heat" + mock_data.reset_mock() + + mock_data.side_effect = SensiboError("info") + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() + + mock_data.return_value = SensiboData(raw={}, parsed={}) + mock_data.side_effect = None + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=3)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + + mock_data.return_value = DATA_FROM_API + mock_data.side_effect = None + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == "heat" + mock_data.reset_mock() + + mock_data.side_effect = AuthenticationError("info") + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=7)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py new file mode 100644 index 00000000000..b4e85dad2b4 --- /dev/null +++ b/tests/components/sensibo/test_diagnostics.py @@ -0,0 +1,48 @@ +"""Test Sensibo diagnostics.""" +from __future__ import annotations + +import aiohttp + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: aiohttp.client, load_int: ConfigEntry +): + """Test generating diagnostics for a config entry.""" + entry = load_int + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == { + "status": "success", + "result": [ + { + "id": "**REDACTED**", + "qrId": "**REDACTED**", + "room": {"uid": "**REDACTED**", "name": "Hallway", "icon": "Lounge"}, + "acState": { + "timestamp": { + "time": "2022-04-30T19:58:15.544787Z", + "secondsAgo": 0, + }, + "on": False, + "mode": "fan", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "location": "**REDACTED**", + "accessPoint": {"ssid": "**REDACTED**", "password": None}, + "macAddress": "**REDACTED**", + "autoOffMinutes": None, + "autoOffEnabled": False, + "antiMoldTimer": None, + "antiMoldConfig": None, + } + ], + } diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py new file mode 100644 index 00000000000..7df70c7d45e --- /dev/null +++ b/tests/components/sensibo/test_entity.py @@ -0,0 +1,132 @@ +"""The test for the sensibo entity.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, +) +from homeassistant.components.number.const import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.components.sensibo.const import SENSIBO_ERRORS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt + +from .response import DATA_FROM_API + +from tests.common import async_fire_time_changed + + +async def test_entity(hass: HomeAssistant, load_int: ConfigEntry) -> None: + """Test the Sensibo climate.""" + + state1 = hass.states.get("climate.hallway") + assert state1 + + dr_reg = dr.async_get(hass) + dr_entries = dr.async_entries_for_config_entry(dr_reg, load_int.entry_id) + dr_entry: dr.DeviceEntry + for dr_entry in dr_entries: + if dr_entry.name == "Hallway": + assert dr_entry.identifiers == {("sensibo", "ABC999111")} + device_id = dr_entry.id + + er_reg = er.async_get(hass) + er_entries = er.async_entries_for_device( + er_reg, device_id, include_disabled_entities=True + ) + er_entry: er.RegistryEntry + for er_entry in er_entries: + if er_entry.name == "Hallway": + assert er_entry.unique_id == "Hallway" + + +@pytest.mark.parametrize("p_error", SENSIBO_ERRORS) +async def test_entity_send_command( + hass: HomeAssistant, p_error: Exception, load_int: ConfigEntry +) -> None: + """Test the Sensibo send command with error.""" + + state = hass.states.get("climate.hallway") + assert state + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "low" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + side_effect=p_error, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "low" + + +async def test_entity_send_command_calibration( + hass: HomeAssistant, load_int: ConfigEntry +) -> None: + """Test the Sensibo send command for calibration.""" + + registry = er.async_get(hass) + registry.async_update_entity( + "number.hallway_temperature_calibration", disabled_by=None + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.1" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", + return_value={"status": "success"}, + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: 0.2}, + blocking=True, + ) + + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.2" diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py new file mode 100644 index 00000000000..b9ad56eaf07 --- /dev/null +++ b/tests/components/sensibo/test_init.py @@ -0,0 +1,132 @@ +"""Test for Sensibo component Init.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.components.sensibo.util import NoUsernameError +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG +from .response import DATA_FROM_API + +from tests.common import MockConfigEntry + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version=2, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + + +async def test_migrate_entry(hass: HomeAssistant) -> None: + """Test migrate entry unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.version == 2 + assert entry.unique_id == "username" + + +async def test_migrate_entry_fails(hass: HomeAssistant) -> None: + """Test migrate entry unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + side_effect=NoUsernameError("No username returned"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.version == 1 + assert entry.unique_id == "12" + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test unload an entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version="2", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED