From 0d957ad93b6aeb437caaebc9f7b4cb64a4eda7c7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 27 Dec 2021 19:34:00 +0100 Subject: [PATCH] Code improvements Sensibo (#62810) --- homeassistant/components/sensibo/__init__.py | 8 +- homeassistant/components/sensibo/climate.py | 246 +++++++----------- .../components/sensibo/config_flow.py | 17 +- homeassistant/components/sensibo/const.py | 6 +- tests/components/sensibo/test_config_flow.py | 9 +- 5 files changed, 105 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index c384c826859..7401a8c2150 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -24,11 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = pysensibo.SensiboClient( entry.data[CONF_API_KEY], session=async_get_clientsession(hass), timeout=TIMEOUT ) - devicelist = [] + devices = [] try: async with async_timeout.timeout(TIMEOUT): for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): - devicelist.append(dev) + devices.append(dev) except ( aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError, @@ -38,11 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Failed to get devices from Sensibo servers: {err}" ) from err - if not devicelist: + if not devices: return False hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "devices": devicelist, + "devices": devices, "client": client, } diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index ac907352735..fd577099627 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,11 +1,13 @@ """Support for Sensibo wifi-enabled home thermostats.""" +from __future__ import annotations import asyncio import logging +from typing import Any import aiohttp import async_timeout -import pysensibo +from pysensibo import SensiboClient, SensiboError import voluptuous as vol from homeassistant.components.climate import ( @@ -73,6 +75,7 @@ SENSIBO_TO_HA = { "fan": HVAC_MODE_FAN_ONLY, "auto": HVAC_MODE_HEAT_COOL, "dry": HVAC_MODE_DRY, + "": HVAC_MODE_OFF, } HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} @@ -83,7 +86,7 @@ async def async_setup_platform( config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType = None, -): +) -> None: """Set up Sensibo devices.""" _LOGGER.warning( "Loading Sensibo via platform setup is deprecated; Please remove it from your configuration" @@ -104,23 +107,23 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] client = data["client"] - devicelist = data["devices"] + devices = data["devices"] - devices = [ + entities = [ SensiboClimate(client, dev, hass.config.units.temperature_unit) - for dev in devicelist + for dev in devices ] - async_add_entities(devices) + async_add_entities(entities) async def async_assume_state(service): """Set state according to external service call..""" if entity_ids := service.data.get(ATTR_ENTITY_ID): target_climate = [ - device for device in devices if device.entity_id in entity_ids + entity for entity in entities if entity.entity_id in entity_ids ] else: - target_climate = devices + target_climate = entities update_tasks = [] for climate in target_climate: @@ -141,44 +144,63 @@ async def async_setup_entry( class SensiboClimate(ClimateEntity): """Representation of a Sensibo device.""" - def __init__(self, client, data, units): - """Build SensiboClimate. - - client: aiohttp session. - data: initially-fetched data. - """ + def __init__(self, client: SensiboClient, data: dict[str, Any], units: str) -> None: + """Initiate SensiboClimate.""" self._client = client self._id = data["id"] self._external_state = None self._units = units - self._available = False - self._do_update(data) self._failed_update = False + self._attr_available = False + self._attr_unique_id = self._id + self._attr_temperature_unit = ( + TEMP_CELSIUS if data["temperatureUnit"] == "C" else TEMP_FAHRENHEIT + ) + self._do_update(data) + self._attr_target_temperature_step = ( + 1 if self.temperature_unit == units else None + ) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._id)}, - name=self._name, + name=self._attr_name, manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", model=data["productModel"], sw_version=data["firmwareVersion"], hw_version=data["firmwareType"], - suggested_area=self._name, + suggested_area=self._attr_name, ) - @property - def supported_features(self): - """Return the list of supported features.""" - return self._supported_features - - def _do_update(self, data): - self._name = data["room"]["name"] - self._measurements = data["measurements"] + def _do_update(self, data) -> None: + self._attr_name = data["room"]["name"] self._ac_states = data["acState"] - self._available = data["connectionStatus"]["isAlive"] + self._attr_extra_state_attributes = { + "battery": data["measurements"].get("batteryVoltage") + } + self._attr_current_temperature = convert_temperature( + data["measurements"].get("temperature"), + TEMP_CELSIUS, + self._attr_temperature_unit, + ) + self._attr_current_humidity = data["measurements"].get("humidity") + + self._attr_target_temperature = self._ac_states.get("targetTemperature") + if self._ac_states["on"]: + self._attr_hvac_mode = SENSIBO_TO_HA.get(self._ac_states["mode"], "") + else: + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_fan_mode = self._ac_states.get("fanLevel") + self._attr_swing_mode = self._ac_states.get("swing") + + self._attr_available = data["connectionStatus"].get("isAlive") capabilities = data["remoteCapabilities"] - self._operations = [SENSIBO_TO_HA[mode] for mode in capabilities["modes"]] - self._operations.append(HVAC_MODE_OFF) - self._current_capabilities = capabilities["modes"][self._ac_states["mode"]] + self._attr_hvac_modes = [SENSIBO_TO_HA[mode] for mode in capabilities["modes"]] + self._attr_hvac_modes.append(HVAC_MODE_OFF) + + current_capabilities = capabilities["modes"][self._ac_states.get("mode")] + self._attr_fan_modes = current_capabilities.get("fanLevels") + self._attr_swing_modes = current_capabilities.get("swing") + temperature_unit_key = data.get("temperatureUnit") or self._ac_states.get( "temperatureUnit" ) @@ -187,129 +209,29 @@ class SensiboClimate(ClimateEntity): TEMP_CELSIUS if temperature_unit_key == "C" else TEMP_FAHRENHEIT ) self._temperatures_list = ( - self._current_capabilities["temperatures"] + current_capabilities["temperatures"] .get(temperature_unit_key, {}) .get("values", []) ) else: self._temperature_unit = self._units self._temperatures_list = [] - self._supported_features = 0 - for key in self._ac_states: - if key in FIELD_TO_FLAG: - self._supported_features |= FIELD_TO_FLAG[key] - - @property - def state(self): - """Return the current state.""" - return self._external_state or super().state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {"battery": self.current_battery} - - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return self._temperature_unit - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._ac_states.get("targetTemperature") - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - if self.temperature_unit == self.hass.config.units.temperature_unit: - # We are working in same units as the a/c unit. Use whole degrees - # like the API supports. - return 1 - # Unit conversion is going on. No point to stick to specific steps. - return None - - @property - def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - if not self._ac_states["on"]: - return HVAC_MODE_OFF - return SENSIBO_TO_HA.get(self._ac_states["mode"]) - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._measurements["humidity"] - - @property - def current_battery(self): - """Return the current battery voltage.""" - return self._measurements.get("batteryVoltage") - - @property - def current_temperature(self): - """Return the current temperature.""" - # This field is not affected by temperatureUnit. - # It is always in C - return convert_temperature( - self._measurements["temperature"], TEMP_CELSIUS, self.temperature_unit - ) - - @property - def hvac_modes(self): - """List of available operation modes.""" - return self._operations - - @property - def fan_mode(self): - """Return the fan setting.""" - return self._ac_states.get("fanLevel") - - @property - def fan_modes(self): - """List of available fan modes.""" - return self._current_capabilities.get("fanLevels") - - @property - def swing_mode(self): - """Return the fan setting.""" - return self._ac_states.get("swing") - - @property - def swing_modes(self): - """List of available swing modes.""" - return self._current_capabilities.get("swing") - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def min_temp(self): - """Return the minimum temperature.""" - return ( + self._attr_min_temp = ( self._temperatures_list[0] if self._temperatures_list else super().min_temp ) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return ( + self._attr_max_temp = ( self._temperatures_list[-1] if self._temperatures_list else super().max_temp ) + self._attr_temperature_unit = self._temperature_unit - @property - def unique_id(self): - """Return unique ID based on Sensibo ID.""" - return self._id + self._attr_supported_features = 0 + for key in self._ac_states: + if key in FIELD_TO_FLAG: + self._attr_supported_features |= FIELD_TO_FLAG[key] - async def async_set_temperature(self, **kwargs): + self._attr_state = self._external_state or super().state + + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -331,11 +253,11 @@ class SensiboClimate(ClimateEntity): await self._async_set_ac_state_property("targetTemperature", temperature) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode) -> None: """Set new target fan mode.""" await self._async_set_ac_state_property("fanLevel", fan_mode) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: await self._async_set_ac_state_property("on", False) @@ -347,19 +269,19 @@ class SensiboClimate(ClimateEntity): await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode) -> None: """Set new target swing operation.""" await self._async_set_ac_state_property("swing", swing_mode) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn Sensibo unit on.""" await self._async_set_ac_state_property("on", True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn Sensibo unit on.""" await self._async_set_ac_state_property("on", False) - async def async_assume_state(self, state): + async def async_assume_state(self, state) -> None: """Set external state.""" change_needed = (state != HVAC_MODE_OFF and not self._ac_states["on"]) or ( state == HVAC_MODE_OFF and self._ac_states["on"] @@ -373,7 +295,7 @@ class SensiboClimate(ClimateEntity): else: self._external_state = state - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" try: async with async_timeout.timeout(TIMEOUT): @@ -381,25 +303,33 @@ class SensiboClimate(ClimateEntity): except ( aiohttp.client_exceptions.ClientError, asyncio.TimeoutError, - pysensibo.SensiboError, - ): + SensiboError, + ) as err: if self._failed_update: _LOGGER.warning( - "Failed to update data for device '%s' from Sensibo servers", - self.name, + "Failed to update data for device '%s' from Sensibo servers with error %s", + self._attr_name, + err, ) - self._available = False + self._attr_available = False self.async_write_ha_state() return - _LOGGER.debug("First failed update data for device '%s'", self.name) + _LOGGER.debug("First failed update data for device '%s'", self._attr_name) self._failed_update = True return + if self.temperature_unit == self.hass.config.units.temperature_unit: + self._attr_target_temperature_step = 1 + else: + self._attr_target_temperature_step = None + self._failed_update = False self._do_update(data) - async def _async_set_ac_state_property(self, name, value, assumed_state=False): + async def _async_set_ac_state_property( + self, name, value, assumed_state=False + ) -> None: """Set AC state.""" try: async with async_timeout.timeout(TIMEOUT): @@ -409,10 +339,10 @@ class SensiboClimate(ClimateEntity): except ( aiohttp.client_exceptions.ClientError, asyncio.TimeoutError, - pysensibo.SensiboError, + SensiboError, ) as err: - self._available = False + self._attr_available = False self.async_write_ha_state() raise Exception( - f"Failed to set AC state for device {self.name} to Sensibo servers" + f"Failed to set AC state for device {self._attr_name} to Sensibo servers" ) from err diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index 0d9e7880f38..77f1049d8d2 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -10,8 +10,9 @@ from pysensibo import SensiboClient, SensiboError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, } ) @@ -53,25 +53,22 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, config: dict): + async def async_step_import(self, config: dict) -> FlowResult: """Import a configuration from config.yaml.""" self.context.update( {"title_placeholders": {"Sensibo": f"YAML import {DOMAIN}"}} ) - if CONF_NAME not in config: - config[CONF_NAME] = DEFAULT_NAME return await self.async_step_user(user_input=config) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" errors: dict[str, str] = {} - if user_input is not None: + if user_input: api_key = user_input[CONF_API_KEY] - name = user_input[CONF_NAME] await self.async_set_unique_id(api_key) self._abort_if_unique_id_configured() @@ -79,8 +76,8 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): validate = await async_validate_api(self.hass, api_key) if validate: return self.async_create_entry( - title=name, - data={CONF_NAME: name, CONF_API_KEY: api_key}, + title=DEFAULT_NAME, + data={CONF_API_KEY: api_key}, ) errors["base"] = "cannot_connect" diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 7bb8d07b7e8..fb387e64a1a 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -1,9 +1,11 @@ """Constants for Sensibo.""" +from homeassistant.const import Platform + DOMAIN = "sensibo" -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] ALL = ["all"] -DEFAULT_NAME = "Sensibo@Home" +DEFAULT_NAME = "Sensibo" TIMEOUT = 8 _FETCH_FIELDS = ",".join( [ diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index b277ed80e96..cf3716f09e4 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -9,7 +9,7 @@ from pysensibo import SensiboError import pytest from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -47,7 +47,6 @@ async def test_form(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_NAME: "Sensibo@Home", CONF_API_KEY: "1234567890", }, ) @@ -55,7 +54,6 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { - "name": "Sensibo@Home", "api_key": "1234567890", } @@ -82,9 +80,8 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Sensibo@Home" + assert result2["title"] == "Sensibo" assert result2["data"] == { - "name": "Sensibo@Home", "api_key": "1234567890", } assert len(mock_setup_entry.mock_calls) == 1 @@ -96,7 +93,6 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: MockConfigEntry( domain=DOMAIN, data={ - CONF_NAME: "Sensibo@Home", CONF_API_KEY: "1234567890", }, unique_id="1234567890", @@ -147,7 +143,6 @@ async def test_flow_fails(hass: HomeAssistant, error_message) -> None: result4 = await hass.config_entries.flow.async_configure( result4["flow_id"], user_input={ - CONF_NAME: "Sensibo@Home", CONF_API_KEY: "1234567890", }, )